##// END OF EJS Templates
Merged r15431 to r15435 (#22924, #22925, #22926)....
Jean-Philippe Lang -
r15058:8c09e330d42d
parent child
Show More
@@ -0,0 +1,35
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require 'uri'
19
20 module Redmine
21 module Helpers
22 module URL
23 def uri_with_safe_scheme?(uri, schemes = ['http', 'https', 'ftp', 'mailto', nil])
24 # URLs relative to the current document or document root (without a protocol
25 # separator, should be harmless
26 return true unless uri.include? ":"
27
28 # Other URLs need to be parsed
29 schemes.include? URI.parse(uri).scheme
30 rescue URI::InvalidURIError
31 false
32 end
33 end
34 end
35 end
@@ -1,1366 +1,1367
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require 'forwardable'
20 require 'forwardable'
21 require 'cgi'
21 require 'cgi'
22
22
23 module ApplicationHelper
23 module ApplicationHelper
24 include Redmine::WikiFormatting::Macros::Definitions
24 include Redmine::WikiFormatting::Macros::Definitions
25 include Redmine::I18n
25 include Redmine::I18n
26 include GravatarHelper::PublicMethods
26 include GravatarHelper::PublicMethods
27 include Redmine::Pagination::Helper
27 include Redmine::Pagination::Helper
28 include Redmine::SudoMode::Helper
28 include Redmine::SudoMode::Helper
29 include Redmine::Themes::Helper
29 include Redmine::Themes::Helper
30 include Redmine::Hook::Helper
30 include Redmine::Hook::Helper
31 include Redmine::Helpers::URL
31
32
32 extend Forwardable
33 extend Forwardable
33 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
34 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
34
35
35 # Return true if user is authorized for controller/action, otherwise false
36 # Return true if user is authorized for controller/action, otherwise false
36 def authorize_for(controller, action)
37 def authorize_for(controller, action)
37 User.current.allowed_to?({:controller => controller, :action => action}, @project)
38 User.current.allowed_to?({:controller => controller, :action => action}, @project)
38 end
39 end
39
40
40 # Display a link if user is authorized
41 # Display a link if user is authorized
41 #
42 #
42 # @param [String] name Anchor text (passed to link_to)
43 # @param [String] name Anchor text (passed to link_to)
43 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
44 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
44 # @param [optional, Hash] html_options Options passed to link_to
45 # @param [optional, Hash] html_options Options passed to link_to
45 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
46 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
46 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
47 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
47 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
48 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
48 end
49 end
49
50
50 # Displays a link to user's account page if active
51 # Displays a link to user's account page if active
51 def link_to_user(user, options={})
52 def link_to_user(user, options={})
52 if user.is_a?(User)
53 if user.is_a?(User)
53 name = h(user.name(options[:format]))
54 name = h(user.name(options[:format]))
54 if user.active? || (User.current.admin? && user.logged?)
55 if user.active? || (User.current.admin? && user.logged?)
55 link_to name, user_path(user), :class => user.css_classes
56 link_to name, user_path(user), :class => user.css_classes
56 else
57 else
57 name
58 name
58 end
59 end
59 else
60 else
60 h(user.to_s)
61 h(user.to_s)
61 end
62 end
62 end
63 end
63
64
64 # Displays a link to +issue+ with its subject.
65 # Displays a link to +issue+ with its subject.
65 # Examples:
66 # Examples:
66 #
67 #
67 # link_to_issue(issue) # => Defect #6: This is the subject
68 # link_to_issue(issue) # => Defect #6: This is the subject
68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
69 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
69 # link_to_issue(issue, :subject => false) # => Defect #6
70 # link_to_issue(issue, :subject => false) # => Defect #6
70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
71 # link_to_issue(issue, :project => true) # => Foo - Defect #6
71 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
72 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
72 #
73 #
73 def link_to_issue(issue, options={})
74 def link_to_issue(issue, options={})
74 title = nil
75 title = nil
75 subject = nil
76 subject = nil
76 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
77 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
77 if options[:subject] == false
78 if options[:subject] == false
78 title = issue.subject.truncate(60)
79 title = issue.subject.truncate(60)
79 else
80 else
80 subject = issue.subject
81 subject = issue.subject
81 if truncate_length = options[:truncate]
82 if truncate_length = options[:truncate]
82 subject = subject.truncate(truncate_length)
83 subject = subject.truncate(truncate_length)
83 end
84 end
84 end
85 end
85 only_path = options[:only_path].nil? ? true : options[:only_path]
86 only_path = options[:only_path].nil? ? true : options[:only_path]
86 s = link_to(text, issue_url(issue, :only_path => only_path),
87 s = link_to(text, issue_url(issue, :only_path => only_path),
87 :class => issue.css_classes, :title => title)
88 :class => issue.css_classes, :title => title)
88 s << h(": #{subject}") if subject
89 s << h(": #{subject}") if subject
89 s = h("#{issue.project} - ") + s if options[:project]
90 s = h("#{issue.project} - ") + s if options[:project]
90 s
91 s
91 end
92 end
92
93
93 # Generates a link to an attachment.
94 # Generates a link to an attachment.
94 # Options:
95 # Options:
95 # * :text - Link text (default to attachment filename)
96 # * :text - Link text (default to attachment filename)
96 # * :download - Force download (default: false)
97 # * :download - Force download (default: false)
97 def link_to_attachment(attachment, options={})
98 def link_to_attachment(attachment, options={})
98 text = options.delete(:text) || attachment.filename
99 text = options.delete(:text) || attachment.filename
99 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
100 route_method = options.delete(:download) ? :download_named_attachment_url : :named_attachment_url
100 html_options = options.slice!(:only_path)
101 html_options = options.slice!(:only_path)
101 options[:only_path] = true unless options.key?(:only_path)
102 options[:only_path] = true unless options.key?(:only_path)
102 url = send(route_method, attachment, attachment.filename, options)
103 url = send(route_method, attachment, attachment.filename, options)
103 link_to text, url, html_options
104 link_to text, url, html_options
104 end
105 end
105
106
106 # Generates a link to a SCM revision
107 # Generates a link to a SCM revision
107 # Options:
108 # Options:
108 # * :text - Link text (default to the formatted revision)
109 # * :text - Link text (default to the formatted revision)
109 def link_to_revision(revision, repository, options={})
110 def link_to_revision(revision, repository, options={})
110 if repository.is_a?(Project)
111 if repository.is_a?(Project)
111 repository = repository.repository
112 repository = repository.repository
112 end
113 end
113 text = options.delete(:text) || format_revision(revision)
114 text = options.delete(:text) || format_revision(revision)
114 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
115 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
115 link_to(
116 link_to(
116 h(text),
117 h(text),
117 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
118 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
118 :title => l(:label_revision_id, format_revision(revision)),
119 :title => l(:label_revision_id, format_revision(revision)),
119 :accesskey => options[:accesskey]
120 :accesskey => options[:accesskey]
120 )
121 )
121 end
122 end
122
123
123 # Generates a link to a message
124 # Generates a link to a message
124 def link_to_message(message, options={}, html_options = nil)
125 def link_to_message(message, options={}, html_options = nil)
125 link_to(
126 link_to(
126 message.subject.truncate(60),
127 message.subject.truncate(60),
127 board_message_url(message.board_id, message.parent_id || message.id, {
128 board_message_url(message.board_id, message.parent_id || message.id, {
128 :r => (message.parent_id && message.id),
129 :r => (message.parent_id && message.id),
129 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
130 :anchor => (message.parent_id ? "message-#{message.id}" : nil),
130 :only_path => true
131 :only_path => true
131 }.merge(options)),
132 }.merge(options)),
132 html_options
133 html_options
133 )
134 )
134 end
135 end
135
136
136 # Generates a link to a project if active
137 # Generates a link to a project if active
137 # Examples:
138 # Examples:
138 #
139 #
139 # link_to_project(project) # => link to the specified project overview
140 # link_to_project(project) # => link to the specified project overview
140 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
141 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
141 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
142 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
142 #
143 #
143 def link_to_project(project, options={}, html_options = nil)
144 def link_to_project(project, options={}, html_options = nil)
144 if project.archived?
145 if project.archived?
145 h(project.name)
146 h(project.name)
146 else
147 else
147 link_to project.name,
148 link_to project.name,
148 project_url(project, {:only_path => true}.merge(options)),
149 project_url(project, {:only_path => true}.merge(options)),
149 html_options
150 html_options
150 end
151 end
151 end
152 end
152
153
153 # Generates a link to a project settings if active
154 # Generates a link to a project settings if active
154 def link_to_project_settings(project, options={}, html_options=nil)
155 def link_to_project_settings(project, options={}, html_options=nil)
155 if project.active?
156 if project.active?
156 link_to project.name, settings_project_path(project, options), html_options
157 link_to project.name, settings_project_path(project, options), html_options
157 elsif project.archived?
158 elsif project.archived?
158 h(project.name)
159 h(project.name)
159 else
160 else
160 link_to project.name, project_path(project, options), html_options
161 link_to project.name, project_path(project, options), html_options
161 end
162 end
162 end
163 end
163
164
164 # Generates a link to a version
165 # Generates a link to a version
165 def link_to_version(version, options = {})
166 def link_to_version(version, options = {})
166 return '' unless version && version.is_a?(Version)
167 return '' unless version && version.is_a?(Version)
167 options = {:title => format_date(version.effective_date)}.merge(options)
168 options = {:title => format_date(version.effective_date)}.merge(options)
168 link_to_if version.visible?, format_version_name(version), version_path(version), options
169 link_to_if version.visible?, format_version_name(version), version_path(version), options
169 end
170 end
170
171
171 # Helper that formats object for html or text rendering
172 # Helper that formats object for html or text rendering
172 def format_object(object, html=true, &block)
173 def format_object(object, html=true, &block)
173 if block_given?
174 if block_given?
174 object = yield object
175 object = yield object
175 end
176 end
176 case object.class.name
177 case object.class.name
177 when 'Array'
178 when 'Array'
178 object.map {|o| format_object(o, html)}.join(', ').html_safe
179 object.map {|o| format_object(o, html)}.join(', ').html_safe
179 when 'Time'
180 when 'Time'
180 format_time(object)
181 format_time(object)
181 when 'Date'
182 when 'Date'
182 format_date(object)
183 format_date(object)
183 when 'Fixnum'
184 when 'Fixnum'
184 object.to_s
185 object.to_s
185 when 'Float'
186 when 'Float'
186 sprintf "%.2f", object
187 sprintf "%.2f", object
187 when 'User'
188 when 'User'
188 html ? link_to_user(object) : object.to_s
189 html ? link_to_user(object) : object.to_s
189 when 'Project'
190 when 'Project'
190 html ? link_to_project(object) : object.to_s
191 html ? link_to_project(object) : object.to_s
191 when 'Version'
192 when 'Version'
192 html ? link_to_version(object) : object.to_s
193 html ? link_to_version(object) : object.to_s
193 when 'TrueClass'
194 when 'TrueClass'
194 l(:general_text_Yes)
195 l(:general_text_Yes)
195 when 'FalseClass'
196 when 'FalseClass'
196 l(:general_text_No)
197 l(:general_text_No)
197 when 'Issue'
198 when 'Issue'
198 object.visible? && html ? link_to_issue(object) : "##{object.id}"
199 object.visible? && html ? link_to_issue(object) : "##{object.id}"
199 when 'CustomValue', 'CustomFieldValue'
200 when 'CustomValue', 'CustomFieldValue'
200 if object.custom_field
201 if object.custom_field
201 f = object.custom_field.format.formatted_custom_value(self, object, html)
202 f = object.custom_field.format.formatted_custom_value(self, object, html)
202 if f.nil? || f.is_a?(String)
203 if f.nil? || f.is_a?(String)
203 f
204 f
204 else
205 else
205 format_object(f, html, &block)
206 format_object(f, html, &block)
206 end
207 end
207 else
208 else
208 object.value.to_s
209 object.value.to_s
209 end
210 end
210 else
211 else
211 html ? h(object) : object.to_s
212 html ? h(object) : object.to_s
212 end
213 end
213 end
214 end
214
215
215 def wiki_page_path(page, options={})
216 def wiki_page_path(page, options={})
216 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
217 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
217 end
218 end
218
219
219 def thumbnail_tag(attachment)
220 def thumbnail_tag(attachment)
220 link_to image_tag(thumbnail_path(attachment)),
221 link_to image_tag(thumbnail_path(attachment)),
221 named_attachment_path(attachment, attachment.filename),
222 named_attachment_path(attachment, attachment.filename),
222 :title => attachment.filename
223 :title => attachment.filename
223 end
224 end
224
225
225 def toggle_link(name, id, options={})
226 def toggle_link(name, id, options={})
226 onclick = "$('##{id}').toggle(); "
227 onclick = "$('##{id}').toggle(); "
227 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
228 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
228 onclick << "return false;"
229 onclick << "return false;"
229 link_to(name, "#", :onclick => onclick)
230 link_to(name, "#", :onclick => onclick)
230 end
231 end
231
232
232 def format_activity_title(text)
233 def format_activity_title(text)
233 h(truncate_single_line_raw(text, 100))
234 h(truncate_single_line_raw(text, 100))
234 end
235 end
235
236
236 def format_activity_day(date)
237 def format_activity_day(date)
237 date == User.current.today ? l(:label_today).titleize : format_date(date)
238 date == User.current.today ? l(:label_today).titleize : format_date(date)
238 end
239 end
239
240
240 def format_activity_description(text)
241 def format_activity_description(text)
241 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
242 h(text.to_s.truncate(120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
242 ).gsub(/[\r\n]+/, "<br />").html_safe
243 ).gsub(/[\r\n]+/, "<br />").html_safe
243 end
244 end
244
245
245 def format_version_name(version)
246 def format_version_name(version)
246 if version.project == @project
247 if version.project == @project
247 h(version)
248 h(version)
248 else
249 else
249 h("#{version.project} - #{version}")
250 h("#{version.project} - #{version}")
250 end
251 end
251 end
252 end
252
253
253 def due_date_distance_in_words(date)
254 def due_date_distance_in_words(date)
254 if date
255 if date
255 l((date < User.current.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(User.current.today, date))
256 l((date < User.current.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(User.current.today, date))
256 end
257 end
257 end
258 end
258
259
259 # Renders a tree of projects as a nested set of unordered lists
260 # Renders a tree of projects as a nested set of unordered lists
260 # The given collection may be a subset of the whole project tree
261 # The given collection may be a subset of the whole project tree
261 # (eg. some intermediate nodes are private and can not be seen)
262 # (eg. some intermediate nodes are private and can not be seen)
262 def render_project_nested_lists(projects, &block)
263 def render_project_nested_lists(projects, &block)
263 s = ''
264 s = ''
264 if projects.any?
265 if projects.any?
265 ancestors = []
266 ancestors = []
266 original_project = @project
267 original_project = @project
267 projects.sort_by(&:lft).each do |project|
268 projects.sort_by(&:lft).each do |project|
268 # set the project environment to please macros.
269 # set the project environment to please macros.
269 @project = project
270 @project = project
270 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
271 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
271 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
272 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
272 else
273 else
273 ancestors.pop
274 ancestors.pop
274 s << "</li>"
275 s << "</li>"
275 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
276 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
276 ancestors.pop
277 ancestors.pop
277 s << "</ul></li>\n"
278 s << "</ul></li>\n"
278 end
279 end
279 end
280 end
280 classes = (ancestors.empty? ? 'root' : 'child')
281 classes = (ancestors.empty? ? 'root' : 'child')
281 s << "<li class='#{classes}'><div class='#{classes}'>"
282 s << "<li class='#{classes}'><div class='#{classes}'>"
282 s << h(block_given? ? capture(project, &block) : project.name)
283 s << h(block_given? ? capture(project, &block) : project.name)
283 s << "</div>\n"
284 s << "</div>\n"
284 ancestors << project
285 ancestors << project
285 end
286 end
286 s << ("</li></ul>\n" * ancestors.size)
287 s << ("</li></ul>\n" * ancestors.size)
287 @project = original_project
288 @project = original_project
288 end
289 end
289 s.html_safe
290 s.html_safe
290 end
291 end
291
292
292 def render_page_hierarchy(pages, node=nil, options={})
293 def render_page_hierarchy(pages, node=nil, options={})
293 content = ''
294 content = ''
294 if pages[node]
295 if pages[node]
295 content << "<ul class=\"pages-hierarchy\">\n"
296 content << "<ul class=\"pages-hierarchy\">\n"
296 pages[node].each do |page|
297 pages[node].each do |page|
297 content << "<li>"
298 content << "<li>"
298 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
299 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
299 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
300 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
300 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
301 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
301 content << "</li>\n"
302 content << "</li>\n"
302 end
303 end
303 content << "</ul>\n"
304 content << "</ul>\n"
304 end
305 end
305 content.html_safe
306 content.html_safe
306 end
307 end
307
308
308 # Renders flash messages
309 # Renders flash messages
309 def render_flash_messages
310 def render_flash_messages
310 s = ''
311 s = ''
311 flash.each do |k,v|
312 flash.each do |k,v|
312 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
313 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
313 end
314 end
314 s.html_safe
315 s.html_safe
315 end
316 end
316
317
317 # Renders tabs and their content
318 # Renders tabs and their content
318 def render_tabs(tabs, selected=params[:tab])
319 def render_tabs(tabs, selected=params[:tab])
319 if tabs.any?
320 if tabs.any?
320 unless tabs.detect {|tab| tab[:name] == selected}
321 unless tabs.detect {|tab| tab[:name] == selected}
321 selected = nil
322 selected = nil
322 end
323 end
323 selected ||= tabs.first[:name]
324 selected ||= tabs.first[:name]
324 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
325 render :partial => 'common/tabs', :locals => {:tabs => tabs, :selected_tab => selected}
325 else
326 else
326 content_tag 'p', l(:label_no_data), :class => "nodata"
327 content_tag 'p', l(:label_no_data), :class => "nodata"
327 end
328 end
328 end
329 end
329
330
330 # Renders the project quick-jump box
331 # Renders the project quick-jump box
331 def render_project_jump_box
332 def render_project_jump_box
332 return unless User.current.logged?
333 return unless User.current.logged?
333 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
334 projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
334 if projects.any?
335 if projects.any?
335 options =
336 options =
336 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
337 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
337 '<option value="" disabled="disabled">---</option>').html_safe
338 '<option value="" disabled="disabled">---</option>').html_safe
338
339
339 options << project_tree_options_for_select(projects, :selected => @project) do |p|
340 options << project_tree_options_for_select(projects, :selected => @project) do |p|
340 { :value => project_path(:id => p, :jump => current_menu_item) }
341 { :value => project_path(:id => p, :jump => current_menu_item) }
341 end
342 end
342
343
343 content_tag( :span, nil, :class => 'jump-box-arrow') +
344 content_tag( :span, nil, :class => 'jump-box-arrow') +
344 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
345 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
345 end
346 end
346 end
347 end
347
348
348 def project_tree_options_for_select(projects, options = {})
349 def project_tree_options_for_select(projects, options = {})
349 s = ''.html_safe
350 s = ''.html_safe
350 if blank_text = options[:include_blank]
351 if blank_text = options[:include_blank]
351 if blank_text == true
352 if blank_text == true
352 blank_text = '&nbsp;'.html_safe
353 blank_text = '&nbsp;'.html_safe
353 end
354 end
354 s << content_tag('option', blank_text, :value => '')
355 s << content_tag('option', blank_text, :value => '')
355 end
356 end
356 project_tree(projects) do |project, level|
357 project_tree(projects) do |project, level|
357 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
358 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
358 tag_options = {:value => project.id}
359 tag_options = {:value => project.id}
359 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
360 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
360 tag_options[:selected] = 'selected'
361 tag_options[:selected] = 'selected'
361 else
362 else
362 tag_options[:selected] = nil
363 tag_options[:selected] = nil
363 end
364 end
364 tag_options.merge!(yield(project)) if block_given?
365 tag_options.merge!(yield(project)) if block_given?
365 s << content_tag('option', name_prefix + h(project), tag_options)
366 s << content_tag('option', name_prefix + h(project), tag_options)
366 end
367 end
367 s.html_safe
368 s.html_safe
368 end
369 end
369
370
370 # Yields the given block for each project with its level in the tree
371 # Yields the given block for each project with its level in the tree
371 #
372 #
372 # Wrapper for Project#project_tree
373 # Wrapper for Project#project_tree
373 def project_tree(projects, &block)
374 def project_tree(projects, &block)
374 Project.project_tree(projects, &block)
375 Project.project_tree(projects, &block)
375 end
376 end
376
377
377 def principals_check_box_tags(name, principals)
378 def principals_check_box_tags(name, principals)
378 s = ''
379 s = ''
379 principals.each do |principal|
380 principals.each do |principal|
380 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
381 s << "<label>#{ check_box_tag name, principal.id, false, :id => nil } #{h principal}</label>\n"
381 end
382 end
382 s.html_safe
383 s.html_safe
383 end
384 end
384
385
385 # Returns a string for users/groups option tags
386 # Returns a string for users/groups option tags
386 def principals_options_for_select(collection, selected=nil)
387 def principals_options_for_select(collection, selected=nil)
387 s = ''
388 s = ''
388 if collection.include?(User.current)
389 if collection.include?(User.current)
389 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
390 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
390 end
391 end
391 groups = ''
392 groups = ''
392 collection.sort.each do |element|
393 collection.sort.each do |element|
393 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
394 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) || element.id.to_s == selected
394 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
395 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
395 end
396 end
396 unless groups.empty?
397 unless groups.empty?
397 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
398 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
398 end
399 end
399 s.html_safe
400 s.html_safe
400 end
401 end
401
402
402 def option_tag(name, text, value, selected=nil, options={})
403 def option_tag(name, text, value, selected=nil, options={})
403 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
404 content_tag 'option', value, options.merge(:value => value, :selected => (value == selected))
404 end
405 end
405
406
406 def truncate_single_line_raw(string, length)
407 def truncate_single_line_raw(string, length)
407 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
408 string.to_s.truncate(length).gsub(%r{[\r\n]+}m, ' ')
408 end
409 end
409
410
410 # Truncates at line break after 250 characters or options[:length]
411 # Truncates at line break after 250 characters or options[:length]
411 def truncate_lines(string, options={})
412 def truncate_lines(string, options={})
412 length = options[:length] || 250
413 length = options[:length] || 250
413 if string.to_s =~ /\A(.{#{length}}.*?)$/m
414 if string.to_s =~ /\A(.{#{length}}.*?)$/m
414 "#{$1}..."
415 "#{$1}..."
415 else
416 else
416 string
417 string
417 end
418 end
418 end
419 end
419
420
420 def anchor(text)
421 def anchor(text)
421 text.to_s.gsub(' ', '_')
422 text.to_s.gsub(' ', '_')
422 end
423 end
423
424
424 def html_hours(text)
425 def html_hours(text)
425 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
426 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
426 end
427 end
427
428
428 def authoring(created, author, options={})
429 def authoring(created, author, options={})
429 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
430 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
430 end
431 end
431
432
432 def time_tag(time)
433 def time_tag(time)
433 text = distance_of_time_in_words(Time.now, time)
434 text = distance_of_time_in_words(Time.now, time)
434 if @project
435 if @project
435 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
436 link_to(text, project_activity_path(@project, :from => User.current.time_to_date(time)), :title => format_time(time))
436 else
437 else
437 content_tag('abbr', text, :title => format_time(time))
438 content_tag('abbr', text, :title => format_time(time))
438 end
439 end
439 end
440 end
440
441
441 def syntax_highlight_lines(name, content)
442 def syntax_highlight_lines(name, content)
442 lines = []
443 lines = []
443 syntax_highlight(name, content).each_line { |line| lines << line }
444 syntax_highlight(name, content).each_line { |line| lines << line }
444 lines
445 lines
445 end
446 end
446
447
447 def syntax_highlight(name, content)
448 def syntax_highlight(name, content)
448 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
449 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
449 end
450 end
450
451
451 def to_path_param(path)
452 def to_path_param(path)
452 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
453 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
453 str.blank? ? nil : str
454 str.blank? ? nil : str
454 end
455 end
455
456
456 def reorder_links(name, url, method = :post)
457 def reorder_links(name, url, method = :post)
457 # TODO: remove associated styles from application.css too
458 # TODO: remove associated styles from application.css too
458 ActiveSupport::Deprecation.warn "Application#reorder_links will be removed in Redmine 4."
459 ActiveSupport::Deprecation.warn "Application#reorder_links will be removed in Redmine 4."
459
460
460 link_to(l(:label_sort_highest),
461 link_to(l(:label_sort_highest),
461 url.merge({"#{name}[move_to]" => 'highest'}), :method => method,
462 url.merge({"#{name}[move_to]" => 'highest'}), :method => method,
462 :title => l(:label_sort_highest), :class => 'icon-only icon-move-top') +
463 :title => l(:label_sort_highest), :class => 'icon-only icon-move-top') +
463 link_to(l(:label_sort_higher),
464 link_to(l(:label_sort_higher),
464 url.merge({"#{name}[move_to]" => 'higher'}), :method => method,
465 url.merge({"#{name}[move_to]" => 'higher'}), :method => method,
465 :title => l(:label_sort_higher), :class => 'icon-only icon-move-up') +
466 :title => l(:label_sort_higher), :class => 'icon-only icon-move-up') +
466 link_to(l(:label_sort_lower),
467 link_to(l(:label_sort_lower),
467 url.merge({"#{name}[move_to]" => 'lower'}), :method => method,
468 url.merge({"#{name}[move_to]" => 'lower'}), :method => method,
468 :title => l(:label_sort_lower), :class => 'icon-only icon-move-down') +
469 :title => l(:label_sort_lower), :class => 'icon-only icon-move-down') +
469 link_to(l(:label_sort_lowest),
470 link_to(l(:label_sort_lowest),
470 url.merge({"#{name}[move_to]" => 'lowest'}), :method => method,
471 url.merge({"#{name}[move_to]" => 'lowest'}), :method => method,
471 :title => l(:label_sort_lowest), :class => 'icon-only icon-move-bottom')
472 :title => l(:label_sort_lowest), :class => 'icon-only icon-move-bottom')
472 end
473 end
473
474
474 def reorder_handle(object, options={})
475 def reorder_handle(object, options={})
475 data = {
476 data = {
476 :reorder_url => options[:url] || url_for(object),
477 :reorder_url => options[:url] || url_for(object),
477 :reorder_param => options[:param] || object.class.name.underscore
478 :reorder_param => options[:param] || object.class.name.underscore
478 }
479 }
479 content_tag('span', '',
480 content_tag('span', '',
480 :class => "sort-handle",
481 :class => "sort-handle",
481 :data => data,
482 :data => data,
482 :title => l(:button_sort))
483 :title => l(:button_sort))
483 end
484 end
484
485
485 def breadcrumb(*args)
486 def breadcrumb(*args)
486 elements = args.flatten
487 elements = args.flatten
487 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
488 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
488 end
489 end
489
490
490 def other_formats_links(&block)
491 def other_formats_links(&block)
491 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
492 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
492 yield Redmine::Views::OtherFormatsBuilder.new(self)
493 yield Redmine::Views::OtherFormatsBuilder.new(self)
493 concat('</p>'.html_safe)
494 concat('</p>'.html_safe)
494 end
495 end
495
496
496 def page_header_title
497 def page_header_title
497 if @project.nil? || @project.new_record?
498 if @project.nil? || @project.new_record?
498 h(Setting.app_title)
499 h(Setting.app_title)
499 else
500 else
500 b = []
501 b = []
501 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
502 ancestors = (@project.root? ? [] : @project.ancestors.visible.to_a)
502 if ancestors.any?
503 if ancestors.any?
503 root = ancestors.shift
504 root = ancestors.shift
504 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
505 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
505 if ancestors.size > 2
506 if ancestors.size > 2
506 b << "\xe2\x80\xa6"
507 b << "\xe2\x80\xa6"
507 ancestors = ancestors[-2, 2]
508 ancestors = ancestors[-2, 2]
508 end
509 end
509 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
510 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
510 end
511 end
511 b << content_tag(:span, h(@project), class: 'current-project')
512 b << content_tag(:span, h(@project), class: 'current-project')
512 if b.size > 1
513 if b.size > 1
513 separator = content_tag(:span, ' &raquo; '.html_safe, class: 'separator')
514 separator = content_tag(:span, ' &raquo; '.html_safe, class: 'separator')
514 path = safe_join(b[0..-2], separator) + separator
515 path = safe_join(b[0..-2], separator) + separator
515 b = [content_tag(:span, path.html_safe, class: 'breadcrumbs'), b[-1]]
516 b = [content_tag(:span, path.html_safe, class: 'breadcrumbs'), b[-1]]
516 end
517 end
517 safe_join b
518 safe_join b
518 end
519 end
519 end
520 end
520
521
521 # Returns a h2 tag and sets the html title with the given arguments
522 # Returns a h2 tag and sets the html title with the given arguments
522 def title(*args)
523 def title(*args)
523 strings = args.map do |arg|
524 strings = args.map do |arg|
524 if arg.is_a?(Array) && arg.size >= 2
525 if arg.is_a?(Array) && arg.size >= 2
525 link_to(*arg)
526 link_to(*arg)
526 else
527 else
527 h(arg.to_s)
528 h(arg.to_s)
528 end
529 end
529 end
530 end
530 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
531 html_title args.reverse.map {|s| (s.is_a?(Array) ? s.first : s).to_s}
531 content_tag('h2', strings.join(' &#187; ').html_safe)
532 content_tag('h2', strings.join(' &#187; ').html_safe)
532 end
533 end
533
534
534 # Sets the html title
535 # Sets the html title
535 # Returns the html title when called without arguments
536 # Returns the html title when called without arguments
536 # Current project name and app_title and automatically appended
537 # Current project name and app_title and automatically appended
537 # Exemples:
538 # Exemples:
538 # html_title 'Foo', 'Bar'
539 # html_title 'Foo', 'Bar'
539 # html_title # => 'Foo - Bar - My Project - Redmine'
540 # html_title # => 'Foo - Bar - My Project - Redmine'
540 def html_title(*args)
541 def html_title(*args)
541 if args.empty?
542 if args.empty?
542 title = @html_title || []
543 title = @html_title || []
543 title << @project.name if @project
544 title << @project.name if @project
544 title << Setting.app_title unless Setting.app_title == title.last
545 title << Setting.app_title unless Setting.app_title == title.last
545 title.reject(&:blank?).join(' - ')
546 title.reject(&:blank?).join(' - ')
546 else
547 else
547 @html_title ||= []
548 @html_title ||= []
548 @html_title += args
549 @html_title += args
549 end
550 end
550 end
551 end
551
552
552 # Returns the theme, controller name, and action as css classes for the
553 # Returns the theme, controller name, and action as css classes for the
553 # HTML body.
554 # HTML body.
554 def body_css_classes
555 def body_css_classes
555 css = []
556 css = []
556 if theme = Redmine::Themes.theme(Setting.ui_theme)
557 if theme = Redmine::Themes.theme(Setting.ui_theme)
557 css << 'theme-' + theme.name
558 css << 'theme-' + theme.name
558 end
559 end
559
560
560 css << 'project-' + @project.identifier if @project && @project.identifier.present?
561 css << 'project-' + @project.identifier if @project && @project.identifier.present?
561 css << 'controller-' + controller_name
562 css << 'controller-' + controller_name
562 css << 'action-' + action_name
563 css << 'action-' + action_name
563 css.join(' ')
564 css.join(' ')
564 end
565 end
565
566
566 def accesskey(s)
567 def accesskey(s)
567 @used_accesskeys ||= []
568 @used_accesskeys ||= []
568 key = Redmine::AccessKeys.key_for(s)
569 key = Redmine::AccessKeys.key_for(s)
569 return nil if @used_accesskeys.include?(key)
570 return nil if @used_accesskeys.include?(key)
570 @used_accesskeys << key
571 @used_accesskeys << key
571 key
572 key
572 end
573 end
573
574
574 # Formats text according to system settings.
575 # Formats text according to system settings.
575 # 2 ways to call this method:
576 # 2 ways to call this method:
576 # * with a String: textilizable(text, options)
577 # * with a String: textilizable(text, options)
577 # * with an object and one of its attribute: textilizable(issue, :description, options)
578 # * with an object and one of its attribute: textilizable(issue, :description, options)
578 def textilizable(*args)
579 def textilizable(*args)
579 options = args.last.is_a?(Hash) ? args.pop : {}
580 options = args.last.is_a?(Hash) ? args.pop : {}
580 case args.size
581 case args.size
581 when 1
582 when 1
582 obj = options[:object]
583 obj = options[:object]
583 text = args.shift
584 text = args.shift
584 when 2
585 when 2
585 obj = args.shift
586 obj = args.shift
586 attr = args.shift
587 attr = args.shift
587 text = obj.send(attr).to_s
588 text = obj.send(attr).to_s
588 else
589 else
589 raise ArgumentError, 'invalid arguments to textilizable'
590 raise ArgumentError, 'invalid arguments to textilizable'
590 end
591 end
591 return '' if text.blank?
592 return '' if text.blank?
592 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
593 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
593 @only_path = only_path = options.delete(:only_path) == false ? false : true
594 @only_path = only_path = options.delete(:only_path) == false ? false : true
594
595
595 text = text.dup
596 text = text.dup
596 macros = catch_macros(text)
597 macros = catch_macros(text)
597 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
598 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
598
599
599 @parsed_headings = []
600 @parsed_headings = []
600 @heading_anchors = {}
601 @heading_anchors = {}
601 @current_section = 0 if options[:edit_section_links]
602 @current_section = 0 if options[:edit_section_links]
602
603
603 parse_sections(text, project, obj, attr, only_path, options)
604 parse_sections(text, project, obj, attr, only_path, options)
604 text = parse_non_pre_blocks(text, obj, macros) do |text|
605 text = parse_non_pre_blocks(text, obj, macros) do |text|
605 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
606 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
606 send method_name, text, project, obj, attr, only_path, options
607 send method_name, text, project, obj, attr, only_path, options
607 end
608 end
608 end
609 end
609 parse_headings(text, project, obj, attr, only_path, options)
610 parse_headings(text, project, obj, attr, only_path, options)
610
611
611 if @parsed_headings.any?
612 if @parsed_headings.any?
612 replace_toc(text, @parsed_headings)
613 replace_toc(text, @parsed_headings)
613 end
614 end
614
615
615 text.html_safe
616 text.html_safe
616 end
617 end
617
618
618 def parse_non_pre_blocks(text, obj, macros)
619 def parse_non_pre_blocks(text, obj, macros)
619 s = StringScanner.new(text)
620 s = StringScanner.new(text)
620 tags = []
621 tags = []
621 parsed = ''
622 parsed = ''
622 while !s.eos?
623 while !s.eos?
623 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
624 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
624 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
625 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
625 if tags.empty?
626 if tags.empty?
626 yield text
627 yield text
627 inject_macros(text, obj, macros) if macros.any?
628 inject_macros(text, obj, macros) if macros.any?
628 else
629 else
629 inject_macros(text, obj, macros, false) if macros.any?
630 inject_macros(text, obj, macros, false) if macros.any?
630 end
631 end
631 parsed << text
632 parsed << text
632 if tag
633 if tag
633 if closing
634 if closing
634 if tags.last && tags.last.casecmp(tag) == 0
635 if tags.last && tags.last.casecmp(tag) == 0
635 tags.pop
636 tags.pop
636 end
637 end
637 else
638 else
638 tags << tag.downcase
639 tags << tag.downcase
639 end
640 end
640 parsed << full_tag
641 parsed << full_tag
641 end
642 end
642 end
643 end
643 # Close any non closing tags
644 # Close any non closing tags
644 while tag = tags.pop
645 while tag = tags.pop
645 parsed << "</#{tag}>"
646 parsed << "</#{tag}>"
646 end
647 end
647 parsed
648 parsed
648 end
649 end
649
650
650 def parse_inline_attachments(text, project, obj, attr, only_path, options)
651 def parse_inline_attachments(text, project, obj, attr, only_path, options)
651 return if options[:inline_attachments] == false
652 return if options[:inline_attachments] == false
652
653
653 # when using an image link, try to use an attachment, if possible
654 # when using an image link, try to use an attachment, if possible
654 attachments = options[:attachments] || []
655 attachments = options[:attachments] || []
655 attachments += obj.attachments if obj.respond_to?(:attachments)
656 attachments += obj.attachments if obj.respond_to?(:attachments)
656 if attachments.present?
657 if attachments.present?
657 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
658 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
658 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
659 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
659 # search for the picture in attachments
660 # search for the picture in attachments
660 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
661 if found = Attachment.latest_attach(attachments, CGI.unescape(filename))
661 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
662 image_url = download_named_attachment_url(found, found.filename, :only_path => only_path)
662 desc = found.description.to_s.gsub('"', '')
663 desc = found.description.to_s.gsub('"', '')
663 if !desc.blank? && alttext.blank?
664 if !desc.blank? && alttext.blank?
664 alt = " title=\"#{desc}\" alt=\"#{desc}\""
665 alt = " title=\"#{desc}\" alt=\"#{desc}\""
665 end
666 end
666 "src=\"#{image_url}\"#{alt}"
667 "src=\"#{image_url}\"#{alt}"
667 else
668 else
668 m
669 m
669 end
670 end
670 end
671 end
671 end
672 end
672 end
673 end
673
674
674 # Wiki links
675 # Wiki links
675 #
676 #
676 # Examples:
677 # Examples:
677 # [[mypage]]
678 # [[mypage]]
678 # [[mypage|mytext]]
679 # [[mypage|mytext]]
679 # wiki links can refer other project wikis, using project name or identifier:
680 # wiki links can refer other project wikis, using project name or identifier:
680 # [[project:]] -> wiki starting page
681 # [[project:]] -> wiki starting page
681 # [[project:|mytext]]
682 # [[project:|mytext]]
682 # [[project:mypage]]
683 # [[project:mypage]]
683 # [[project:mypage|mytext]]
684 # [[project:mypage|mytext]]
684 def parse_wiki_links(text, project, obj, attr, only_path, options)
685 def parse_wiki_links(text, project, obj, attr, only_path, options)
685 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
686 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
686 link_project = project
687 link_project = project
687 esc, all, page, title = $1, $2, $3, $5
688 esc, all, page, title = $1, $2, $3, $5
688 if esc.nil?
689 if esc.nil?
689 if page =~ /^([^\:]+)\:(.*)$/
690 if page =~ /^([^\:]+)\:(.*)$/
690 identifier, page = $1, $2
691 identifier, page = $1, $2
691 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
692 link_project = Project.find_by_identifier(identifier) || Project.find_by_name(identifier)
692 title ||= identifier if page.blank?
693 title ||= identifier if page.blank?
693 end
694 end
694
695
695 if link_project && link_project.wiki
696 if link_project && link_project.wiki
696 # extract anchor
697 # extract anchor
697 anchor = nil
698 anchor = nil
698 if page =~ /^(.+?)\#(.+)$/
699 if page =~ /^(.+?)\#(.+)$/
699 page, anchor = $1, $2
700 page, anchor = $1, $2
700 end
701 end
701 anchor = sanitize_anchor_name(anchor) if anchor.present?
702 anchor = sanitize_anchor_name(anchor) if anchor.present?
702 # check if page exists
703 # check if page exists
703 wiki_page = link_project.wiki.find_page(page)
704 wiki_page = link_project.wiki.find_page(page)
704 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
705 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
705 "##{anchor}"
706 "##{anchor}"
706 else
707 else
707 case options[:wiki_links]
708 case options[:wiki_links]
708 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
709 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
709 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
710 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
710 else
711 else
711 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
712 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
712 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
713 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
713 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
714 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
714 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
715 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
715 end
716 end
716 end
717 end
717 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
718 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
718 else
719 else
719 # project or wiki doesn't exist
720 # project or wiki doesn't exist
720 all
721 all
721 end
722 end
722 else
723 else
723 all
724 all
724 end
725 end
725 end
726 end
726 end
727 end
727
728
728 # Redmine links
729 # Redmine links
729 #
730 #
730 # Examples:
731 # Examples:
731 # Issues:
732 # Issues:
732 # #52 -> Link to issue #52
733 # #52 -> Link to issue #52
733 # Changesets:
734 # Changesets:
734 # r52 -> Link to revision 52
735 # r52 -> Link to revision 52
735 # commit:a85130f -> Link to scmid starting with a85130f
736 # commit:a85130f -> Link to scmid starting with a85130f
736 # Documents:
737 # Documents:
737 # document#17 -> Link to document with id 17
738 # document#17 -> Link to document with id 17
738 # document:Greetings -> Link to the document with title "Greetings"
739 # document:Greetings -> Link to the document with title "Greetings"
739 # document:"Some document" -> Link to the document with title "Some document"
740 # document:"Some document" -> Link to the document with title "Some document"
740 # Versions:
741 # Versions:
741 # version#3 -> Link to version with id 3
742 # version#3 -> Link to version with id 3
742 # version:1.0.0 -> Link to version named "1.0.0"
743 # version:1.0.0 -> Link to version named "1.0.0"
743 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
744 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
744 # Attachments:
745 # Attachments:
745 # attachment:file.zip -> Link to the attachment of the current object named file.zip
746 # attachment:file.zip -> Link to the attachment of the current object named file.zip
746 # Source files:
747 # Source files:
747 # source:some/file -> Link to the file located at /some/file in the project's repository
748 # source:some/file -> Link to the file located at /some/file in the project's repository
748 # source:some/file@52 -> Link to the file's revision 52
749 # source:some/file@52 -> Link to the file's revision 52
749 # source:some/file#L120 -> Link to line 120 of the file
750 # source:some/file#L120 -> Link to line 120 of the file
750 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
751 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
751 # export:some/file -> Force the download of the file
752 # export:some/file -> Force the download of the file
752 # Forum messages:
753 # Forum messages:
753 # message#1218 -> Link to message with id 1218
754 # message#1218 -> Link to message with id 1218
754 # Projects:
755 # Projects:
755 # project:someproject -> Link to project named "someproject"
756 # project:someproject -> Link to project named "someproject"
756 # project#3 -> Link to project with id 3
757 # project#3 -> Link to project with id 3
757 #
758 #
758 # Links can refer other objects from other projects, using project identifier:
759 # Links can refer other objects from other projects, using project identifier:
759 # identifier:r52
760 # identifier:r52
760 # identifier:document:"Some document"
761 # identifier:document:"Some document"
761 # identifier:version:1.0.0
762 # identifier:version:1.0.0
762 # identifier:source:some/file
763 # identifier:source:some/file
763 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
764 def parse_redmine_links(text, default_project, obj, attr, only_path, options)
764 text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
765 text.gsub!(%r{<a( [^>]+?)?>(.*?)</a>|([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-_]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
765 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $2, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
766 tag_content, leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $2, $3, $4, $5, $6, $7, $12, $13, $10 || $14 || $20, $16 || $21, $17, $19
766 if tag_content
767 if tag_content
767 $&
768 $&
768 else
769 else
769 link = nil
770 link = nil
770 project = default_project
771 project = default_project
771 if project_identifier
772 if project_identifier
772 project = Project.visible.find_by_identifier(project_identifier)
773 project = Project.visible.find_by_identifier(project_identifier)
773 end
774 end
774 if esc.nil?
775 if esc.nil?
775 if prefix.nil? && sep == 'r'
776 if prefix.nil? && sep == 'r'
776 if project
777 if project
777 repository = nil
778 repository = nil
778 if repo_identifier
779 if repo_identifier
779 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
780 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
780 else
781 else
781 repository = project.repository
782 repository = project.repository
782 end
783 end
783 # project.changesets.visible raises an SQL error because of a double join on repositories
784 # project.changesets.visible raises an SQL error because of a double join on repositories
784 if repository &&
785 if repository &&
785 (changeset = Changeset.visible.
786 (changeset = Changeset.visible.
786 find_by_repository_id_and_revision(repository.id, identifier))
787 find_by_repository_id_and_revision(repository.id, identifier))
787 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
788 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"),
788 {:only_path => only_path, :controller => 'repositories',
789 {:only_path => only_path, :controller => 'repositories',
789 :action => 'revision', :id => project,
790 :action => 'revision', :id => project,
790 :repository_id => repository.identifier_param,
791 :repository_id => repository.identifier_param,
791 :rev => changeset.revision},
792 :rev => changeset.revision},
792 :class => 'changeset',
793 :class => 'changeset',
793 :title => truncate_single_line_raw(changeset.comments, 100))
794 :title => truncate_single_line_raw(changeset.comments, 100))
794 end
795 end
795 end
796 end
796 elsif sep == '#'
797 elsif sep == '#'
797 oid = identifier.to_i
798 oid = identifier.to_i
798 case prefix
799 case prefix
799 when nil
800 when nil
800 if oid.to_s == identifier &&
801 if oid.to_s == identifier &&
801 issue = Issue.visible.find_by_id(oid)
802 issue = Issue.visible.find_by_id(oid)
802 anchor = comment_id ? "note-#{comment_id}" : nil
803 anchor = comment_id ? "note-#{comment_id}" : nil
803 link = link_to("##{oid}#{comment_suffix}",
804 link = link_to("##{oid}#{comment_suffix}",
804 issue_url(issue, :only_path => only_path, :anchor => anchor),
805 issue_url(issue, :only_path => only_path, :anchor => anchor),
805 :class => issue.css_classes,
806 :class => issue.css_classes,
806 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
807 :title => "#{issue.tracker.name}: #{issue.subject.truncate(100)} (#{issue.status.name})")
807 end
808 end
808 when 'document'
809 when 'document'
809 if document = Document.visible.find_by_id(oid)
810 if document = Document.visible.find_by_id(oid)
810 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
811 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
811 end
812 end
812 when 'version'
813 when 'version'
813 if version = Version.visible.find_by_id(oid)
814 if version = Version.visible.find_by_id(oid)
814 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
815 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
815 end
816 end
816 when 'message'
817 when 'message'
817 if message = Message.visible.find_by_id(oid)
818 if message = Message.visible.find_by_id(oid)
818 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
819 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
819 end
820 end
820 when 'forum'
821 when 'forum'
821 if board = Board.visible.find_by_id(oid)
822 if board = Board.visible.find_by_id(oid)
822 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
823 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
823 end
824 end
824 when 'news'
825 when 'news'
825 if news = News.visible.find_by_id(oid)
826 if news = News.visible.find_by_id(oid)
826 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
827 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
827 end
828 end
828 when 'project'
829 when 'project'
829 if p = Project.visible.find_by_id(oid)
830 if p = Project.visible.find_by_id(oid)
830 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
831 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
831 end
832 end
832 end
833 end
833 elsif sep == ':'
834 elsif sep == ':'
834 # removes the double quotes if any
835 # removes the double quotes if any
835 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
836 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
836 name = CGI.unescapeHTML(name)
837 name = CGI.unescapeHTML(name)
837 case prefix
838 case prefix
838 when 'document'
839 when 'document'
839 if project && document = project.documents.visible.find_by_title(name)
840 if project && document = project.documents.visible.find_by_title(name)
840 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
841 link = link_to(document.title, document_url(document, :only_path => only_path), :class => 'document')
841 end
842 end
842 when 'version'
843 when 'version'
843 if project && version = project.versions.visible.find_by_name(name)
844 if project && version = project.versions.visible.find_by_name(name)
844 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
845 link = link_to(version.name, version_url(version, :only_path => only_path), :class => 'version')
845 end
846 end
846 when 'forum'
847 when 'forum'
847 if project && board = project.boards.visible.find_by_name(name)
848 if project && board = project.boards.visible.find_by_name(name)
848 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
849 link = link_to(board.name, project_board_url(board.project, board, :only_path => only_path), :class => 'board')
849 end
850 end
850 when 'news'
851 when 'news'
851 if project && news = project.news.visible.find_by_title(name)
852 if project && news = project.news.visible.find_by_title(name)
852 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
853 link = link_to(news.title, news_url(news, :only_path => only_path), :class => 'news')
853 end
854 end
854 when 'commit', 'source', 'export'
855 when 'commit', 'source', 'export'
855 if project
856 if project
856 repository = nil
857 repository = nil
857 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
858 if name =~ %r{^(([a-z0-9\-_]+)\|)(.+)$}
858 repo_prefix, repo_identifier, name = $1, $2, $3
859 repo_prefix, repo_identifier, name = $1, $2, $3
859 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
860 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
860 else
861 else
861 repository = project.repository
862 repository = project.repository
862 end
863 end
863 if prefix == 'commit'
864 if prefix == 'commit'
864 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
865 if repository && (changeset = Changeset.visible.where("repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%").first)
865 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
866 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
866 :class => 'changeset',
867 :class => 'changeset',
867 :title => truncate_single_line_raw(changeset.comments, 100)
868 :title => truncate_single_line_raw(changeset.comments, 100)
868 end
869 end
869 else
870 else
870 if repository && User.current.allowed_to?(:browse_repository, project)
871 if repository && User.current.allowed_to?(:browse_repository, project)
871 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
872 name =~ %r{^[/\\]*(.*?)(@([^/\\@]+?))?(#(L\d+))?$}
872 path, rev, anchor = $1, $3, $5
873 path, rev, anchor = $1, $3, $5
873 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
874 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
874 :path => to_path_param(path),
875 :path => to_path_param(path),
875 :rev => rev,
876 :rev => rev,
876 :anchor => anchor},
877 :anchor => anchor},
877 :class => (prefix == 'export' ? 'source download' : 'source')
878 :class => (prefix == 'export' ? 'source download' : 'source')
878 end
879 end
879 end
880 end
880 repo_prefix = nil
881 repo_prefix = nil
881 end
882 end
882 when 'attachment'
883 when 'attachment'
883 attachments = options[:attachments] || []
884 attachments = options[:attachments] || []
884 attachments += obj.attachments if obj.respond_to?(:attachments)
885 attachments += obj.attachments if obj.respond_to?(:attachments)
885 if attachments && attachment = Attachment.latest_attach(attachments, name)
886 if attachments && attachment = Attachment.latest_attach(attachments, name)
886 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
887 link = link_to_attachment(attachment, :only_path => only_path, :download => true, :class => 'attachment')
887 end
888 end
888 when 'project'
889 when 'project'
889 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
890 if p = Project.visible.where("identifier = :s OR LOWER(name) = :s", :s => name.downcase).first
890 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
891 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
891 end
892 end
892 end
893 end
893 end
894 end
894 end
895 end
895 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
896 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
896 end
897 end
897 end
898 end
898 end
899 end
899
900
900 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
901 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
901
902
902 def parse_sections(text, project, obj, attr, only_path, options)
903 def parse_sections(text, project, obj, attr, only_path, options)
903 return unless options[:edit_section_links]
904 return unless options[:edit_section_links]
904 text.gsub!(HEADING_RE) do
905 text.gsub!(HEADING_RE) do
905 heading, level = $1, $2
906 heading, level = $1, $2
906 @current_section += 1
907 @current_section += 1
907 if @current_section > 1
908 if @current_section > 1
908 content_tag('div',
909 content_tag('div',
909 link_to(l(:button_edit_section), options[:edit_section_links].merge(:section => @current_section),
910 link_to(l(:button_edit_section), options[:edit_section_links].merge(:section => @current_section),
910 :class => 'icon-only icon-edit'),
911 :class => 'icon-only icon-edit'),
911 :class => "contextual heading-#{level}",
912 :class => "contextual heading-#{level}",
912 :title => l(:button_edit_section),
913 :title => l(:button_edit_section),
913 :id => "section-#{@current_section}") + heading.html_safe
914 :id => "section-#{@current_section}") + heading.html_safe
914 else
915 else
915 heading
916 heading
916 end
917 end
917 end
918 end
918 end
919 end
919
920
920 # Headings and TOC
921 # Headings and TOC
921 # Adds ids and links to headings unless options[:headings] is set to false
922 # Adds ids and links to headings unless options[:headings] is set to false
922 def parse_headings(text, project, obj, attr, only_path, options)
923 def parse_headings(text, project, obj, attr, only_path, options)
923 return if options[:headings] == false
924 return if options[:headings] == false
924
925
925 text.gsub!(HEADING_RE) do
926 text.gsub!(HEADING_RE) do
926 level, attrs, content = $2.to_i, $3, $4
927 level, attrs, content = $2.to_i, $3, $4
927 item = strip_tags(content).strip
928 item = strip_tags(content).strip
928 anchor = sanitize_anchor_name(item)
929 anchor = sanitize_anchor_name(item)
929 # used for single-file wiki export
930 # used for single-file wiki export
930 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
931 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
931 @heading_anchors[anchor] ||= 0
932 @heading_anchors[anchor] ||= 0
932 idx = (@heading_anchors[anchor] += 1)
933 idx = (@heading_anchors[anchor] += 1)
933 if idx > 1
934 if idx > 1
934 anchor = "#{anchor}-#{idx}"
935 anchor = "#{anchor}-#{idx}"
935 end
936 end
936 @parsed_headings << [level, anchor, item]
937 @parsed_headings << [level, anchor, item]
937 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
938 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
938 end
939 end
939 end
940 end
940
941
941 MACROS_RE = /(
942 MACROS_RE = /(
942 (!)? # escaping
943 (!)? # escaping
943 (
944 (
944 \{\{ # opening tag
945 \{\{ # opening tag
945 ([\w]+) # macro name
946 ([\w]+) # macro name
946 (\(([^\n\r]*?)\))? # optional arguments
947 (\(([^\n\r]*?)\))? # optional arguments
947 ([\n\r].*?[\n\r])? # optional block of text
948 ([\n\r].*?[\n\r])? # optional block of text
948 \}\} # closing tag
949 \}\} # closing tag
949 )
950 )
950 )/mx unless const_defined?(:MACROS_RE)
951 )/mx unless const_defined?(:MACROS_RE)
951
952
952 MACRO_SUB_RE = /(
953 MACRO_SUB_RE = /(
953 \{\{
954 \{\{
954 macro\((\d+)\)
955 macro\((\d+)\)
955 \}\}
956 \}\}
956 )/x unless const_defined?(:MACRO_SUB_RE)
957 )/x unless const_defined?(:MACRO_SUB_RE)
957
958
958 # Extracts macros from text
959 # Extracts macros from text
959 def catch_macros(text)
960 def catch_macros(text)
960 macros = {}
961 macros = {}
961 text.gsub!(MACROS_RE) do
962 text.gsub!(MACROS_RE) do
962 all, macro = $1, $4.downcase
963 all, macro = $1, $4.downcase
963 if macro_exists?(macro) || all =~ MACRO_SUB_RE
964 if macro_exists?(macro) || all =~ MACRO_SUB_RE
964 index = macros.size
965 index = macros.size
965 macros[index] = all
966 macros[index] = all
966 "{{macro(#{index})}}"
967 "{{macro(#{index})}}"
967 else
968 else
968 all
969 all
969 end
970 end
970 end
971 end
971 macros
972 macros
972 end
973 end
973
974
974 # Executes and replaces macros in text
975 # Executes and replaces macros in text
975 def inject_macros(text, obj, macros, execute=true)
976 def inject_macros(text, obj, macros, execute=true)
976 text.gsub!(MACRO_SUB_RE) do
977 text.gsub!(MACRO_SUB_RE) do
977 all, index = $1, $2.to_i
978 all, index = $1, $2.to_i
978 orig = macros.delete(index)
979 orig = macros.delete(index)
979 if execute && orig && orig =~ MACROS_RE
980 if execute && orig && orig =~ MACROS_RE
980 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
981 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
981 if esc.nil?
982 if esc.nil?
982 h(exec_macro(macro, obj, args, block) || all)
983 h(exec_macro(macro, obj, args, block) || all)
983 else
984 else
984 h(all)
985 h(all)
985 end
986 end
986 elsif orig
987 elsif orig
987 h(orig)
988 h(orig)
988 else
989 else
989 h(all)
990 h(all)
990 end
991 end
991 end
992 end
992 end
993 end
993
994
994 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
995 TOC_RE = /<p>\{\{((<|&lt;)|(>|&gt;))?toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
995
996
996 # Renders the TOC with given headings
997 # Renders the TOC with given headings
997 def replace_toc(text, headings)
998 def replace_toc(text, headings)
998 text.gsub!(TOC_RE) do
999 text.gsub!(TOC_RE) do
999 left_align, right_align = $2, $3
1000 left_align, right_align = $2, $3
1000 # Keep only the 4 first levels
1001 # Keep only the 4 first levels
1001 headings = headings.select{|level, anchor, item| level <= 4}
1002 headings = headings.select{|level, anchor, item| level <= 4}
1002 if headings.empty?
1003 if headings.empty?
1003 ''
1004 ''
1004 else
1005 else
1005 div_class = 'toc'
1006 div_class = 'toc'
1006 div_class << ' right' if right_align
1007 div_class << ' right' if right_align
1007 div_class << ' left' if left_align
1008 div_class << ' left' if left_align
1008 out = "<ul class=\"#{div_class}\"><li>"
1009 out = "<ul class=\"#{div_class}\"><li>"
1009 root = headings.map(&:first).min
1010 root = headings.map(&:first).min
1010 current = root
1011 current = root
1011 started = false
1012 started = false
1012 headings.each do |level, anchor, item|
1013 headings.each do |level, anchor, item|
1013 if level > current
1014 if level > current
1014 out << '<ul><li>' * (level - current)
1015 out << '<ul><li>' * (level - current)
1015 elsif level < current
1016 elsif level < current
1016 out << "</li></ul>\n" * (current - level) + "</li><li>"
1017 out << "</li></ul>\n" * (current - level) + "</li><li>"
1017 elsif started
1018 elsif started
1018 out << '</li><li>'
1019 out << '</li><li>'
1019 end
1020 end
1020 out << "<a href=\"##{anchor}\">#{item}</a>"
1021 out << "<a href=\"##{anchor}\">#{item}</a>"
1021 current = level
1022 current = level
1022 started = true
1023 started = true
1023 end
1024 end
1024 out << '</li></ul>' * (current - root)
1025 out << '</li></ul>' * (current - root)
1025 out << '</li></ul>'
1026 out << '</li></ul>'
1026 end
1027 end
1027 end
1028 end
1028 end
1029 end
1029
1030
1030 # Same as Rails' simple_format helper without using paragraphs
1031 # Same as Rails' simple_format helper without using paragraphs
1031 def simple_format_without_paragraph(text)
1032 def simple_format_without_paragraph(text)
1032 text.to_s.
1033 text.to_s.
1033 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1034 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
1034 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1035 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
1035 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1036 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
1036 html_safe
1037 html_safe
1037 end
1038 end
1038
1039
1039 def lang_options_for_select(blank=true)
1040 def lang_options_for_select(blank=true)
1040 (blank ? [["(auto)", ""]] : []) + languages_options
1041 (blank ? [["(auto)", ""]] : []) + languages_options
1041 end
1042 end
1042
1043
1043 def labelled_form_for(*args, &proc)
1044 def labelled_form_for(*args, &proc)
1044 args << {} unless args.last.is_a?(Hash)
1045 args << {} unless args.last.is_a?(Hash)
1045 options = args.last
1046 options = args.last
1046 if args.first.is_a?(Symbol)
1047 if args.first.is_a?(Symbol)
1047 options.merge!(:as => args.shift)
1048 options.merge!(:as => args.shift)
1048 end
1049 end
1049 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1050 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1050 form_for(*args, &proc)
1051 form_for(*args, &proc)
1051 end
1052 end
1052
1053
1053 def labelled_fields_for(*args, &proc)
1054 def labelled_fields_for(*args, &proc)
1054 args << {} unless args.last.is_a?(Hash)
1055 args << {} unless args.last.is_a?(Hash)
1055 options = args.last
1056 options = args.last
1056 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1057 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
1057 fields_for(*args, &proc)
1058 fields_for(*args, &proc)
1058 end
1059 end
1059
1060
1060 # Render the error messages for the given objects
1061 # Render the error messages for the given objects
1061 def error_messages_for(*objects)
1062 def error_messages_for(*objects)
1062 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1063 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1063 errors = objects.map {|o| o.errors.full_messages}.flatten
1064 errors = objects.map {|o| o.errors.full_messages}.flatten
1064 render_error_messages(errors)
1065 render_error_messages(errors)
1065 end
1066 end
1066
1067
1067 # Renders a list of error messages
1068 # Renders a list of error messages
1068 def render_error_messages(errors)
1069 def render_error_messages(errors)
1069 html = ""
1070 html = ""
1070 if errors.present?
1071 if errors.present?
1071 html << "<div id='errorExplanation'><ul>\n"
1072 html << "<div id='errorExplanation'><ul>\n"
1072 errors.each do |error|
1073 errors.each do |error|
1073 html << "<li>#{h error}</li>\n"
1074 html << "<li>#{h error}</li>\n"
1074 end
1075 end
1075 html << "</ul></div>\n"
1076 html << "</ul></div>\n"
1076 end
1077 end
1077 html.html_safe
1078 html.html_safe
1078 end
1079 end
1079
1080
1080 def delete_link(url, options={})
1081 def delete_link(url, options={})
1081 options = {
1082 options = {
1082 :method => :delete,
1083 :method => :delete,
1083 :data => {:confirm => l(:text_are_you_sure)},
1084 :data => {:confirm => l(:text_are_you_sure)},
1084 :class => 'icon icon-del'
1085 :class => 'icon icon-del'
1085 }.merge(options)
1086 }.merge(options)
1086
1087
1087 link_to l(:button_delete), url, options
1088 link_to l(:button_delete), url, options
1088 end
1089 end
1089
1090
1090 def preview_link(url, form, target='preview', options={})
1091 def preview_link(url, form, target='preview', options={})
1091 content_tag 'a', l(:label_preview), {
1092 content_tag 'a', l(:label_preview), {
1092 :href => "#",
1093 :href => "#",
1093 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1094 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1094 :accesskey => accesskey(:preview)
1095 :accesskey => accesskey(:preview)
1095 }.merge(options)
1096 }.merge(options)
1096 end
1097 end
1097
1098
1098 def link_to_function(name, function, html_options={})
1099 def link_to_function(name, function, html_options={})
1099 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1100 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1100 end
1101 end
1101
1102
1102 # Helper to render JSON in views
1103 # Helper to render JSON in views
1103 def raw_json(arg)
1104 def raw_json(arg)
1104 arg.to_json.to_s.gsub('/', '\/').html_safe
1105 arg.to_json.to_s.gsub('/', '\/').html_safe
1105 end
1106 end
1106
1107
1107 def back_url
1108 def back_url
1108 url = params[:back_url]
1109 url = params[:back_url]
1109 if url.nil? && referer = request.env['HTTP_REFERER']
1110 if url.nil? && referer = request.env['HTTP_REFERER']
1110 url = CGI.unescape(referer.to_s)
1111 url = CGI.unescape(referer.to_s)
1111 end
1112 end
1112 url
1113 url
1113 end
1114 end
1114
1115
1115 def back_url_hidden_field_tag
1116 def back_url_hidden_field_tag
1116 url = back_url
1117 url = back_url
1117 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1118 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1118 end
1119 end
1119
1120
1120 def check_all_links(form_name)
1121 def check_all_links(form_name)
1121 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1122 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1122 " | ".html_safe +
1123 " | ".html_safe +
1123 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1124 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1124 end
1125 end
1125
1126
1126 def toggle_checkboxes_link(selector)
1127 def toggle_checkboxes_link(selector)
1127 link_to_function '',
1128 link_to_function '',
1128 "toggleCheckboxesBySelector('#{selector}')",
1129 "toggleCheckboxesBySelector('#{selector}')",
1129 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}",
1130 :title => "#{l(:button_check_all)} / #{l(:button_uncheck_all)}",
1130 :class => 'toggle-checkboxes'
1131 :class => 'toggle-checkboxes'
1131 end
1132 end
1132
1133
1133 def progress_bar(pcts, options={})
1134 def progress_bar(pcts, options={})
1134 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1135 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1135 pcts = pcts.collect(&:round)
1136 pcts = pcts.collect(&:round)
1136 pcts[1] = pcts[1] - pcts[0]
1137 pcts[1] = pcts[1] - pcts[0]
1137 pcts << (100 - pcts[1] - pcts[0])
1138 pcts << (100 - pcts[1] - pcts[0])
1138 titles = options[:titles].to_a
1139 titles = options[:titles].to_a
1139 titles[0] = "#{pcts[0]}%" if titles[0].blank?
1140 titles[0] = "#{pcts[0]}%" if titles[0].blank?
1140 legend = options[:legend] || ''
1141 legend = options[:legend] || ''
1141 content_tag('table',
1142 content_tag('table',
1142 content_tag('tr',
1143 content_tag('tr',
1143 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed', :title => titles[0]) : ''.html_safe) +
1144 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed', :title => titles[0]) : ''.html_safe) +
1144 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done', :title => titles[1]) : ''.html_safe) +
1145 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done', :title => titles[1]) : ''.html_safe) +
1145 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo', :title => titles[2]) : ''.html_safe)
1146 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo', :title => titles[2]) : ''.html_safe)
1146 ), :class => "progress progress-#{pcts[0]}").html_safe +
1147 ), :class => "progress progress-#{pcts[0]}").html_safe +
1147 content_tag('p', legend, :class => 'percent').html_safe
1148 content_tag('p', legend, :class => 'percent').html_safe
1148 end
1149 end
1149
1150
1150 def checked_image(checked=true)
1151 def checked_image(checked=true)
1151 if checked
1152 if checked
1152 @checked_image_tag ||= content_tag(:span, nil, :class => 'icon-only icon-checked')
1153 @checked_image_tag ||= content_tag(:span, nil, :class => 'icon-only icon-checked')
1153 end
1154 end
1154 end
1155 end
1155
1156
1156 def context_menu(url)
1157 def context_menu(url)
1157 unless @context_menu_included
1158 unless @context_menu_included
1158 content_for :header_tags do
1159 content_for :header_tags do
1159 javascript_include_tag('context_menu') +
1160 javascript_include_tag('context_menu') +
1160 stylesheet_link_tag('context_menu')
1161 stylesheet_link_tag('context_menu')
1161 end
1162 end
1162 if l(:direction) == 'rtl'
1163 if l(:direction) == 'rtl'
1163 content_for :header_tags do
1164 content_for :header_tags do
1164 stylesheet_link_tag('context_menu_rtl')
1165 stylesheet_link_tag('context_menu_rtl')
1165 end
1166 end
1166 end
1167 end
1167 @context_menu_included = true
1168 @context_menu_included = true
1168 end
1169 end
1169 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1170 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1170 end
1171 end
1171
1172
1172 def calendar_for(field_id)
1173 def calendar_for(field_id)
1173 include_calendar_headers_tags
1174 include_calendar_headers_tags
1174 javascript_tag("$(function() { $('##{field_id}').addClass('date').datepickerFallback(datepickerOptions); });")
1175 javascript_tag("$(function() { $('##{field_id}').addClass('date').datepickerFallback(datepickerOptions); });")
1175 end
1176 end
1176
1177
1177 def include_calendar_headers_tags
1178 def include_calendar_headers_tags
1178 unless @calendar_headers_tags_included
1179 unless @calendar_headers_tags_included
1179 tags = ''.html_safe
1180 tags = ''.html_safe
1180 @calendar_headers_tags_included = true
1181 @calendar_headers_tags_included = true
1181 content_for :header_tags do
1182 content_for :header_tags do
1182 start_of_week = Setting.start_of_week
1183 start_of_week = Setting.start_of_week
1183 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1184 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1184 # Redmine uses 1..7 (monday..sunday) in settings and locales
1185 # Redmine uses 1..7 (monday..sunday) in settings and locales
1185 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1186 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1186 start_of_week = start_of_week.to_i % 7
1187 start_of_week = start_of_week.to_i % 7
1187 tags << javascript_tag(
1188 tags << javascript_tag(
1188 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1189 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1189 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1190 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1190 path_to_image('/images/calendar.png') +
1191 path_to_image('/images/calendar.png') +
1191 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1192 "', showButtonPanel: true, showWeek: true, showOtherMonths: true, " +
1192 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1193 "selectOtherMonths: true, changeMonth: true, changeYear: true, " +
1193 "beforeShow: beforeShowDatePicker};")
1194 "beforeShow: beforeShowDatePicker};")
1194 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1195 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1195 unless jquery_locale == 'en'
1196 unless jquery_locale == 'en'
1196 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1197 tags << javascript_include_tag("i18n/datepicker-#{jquery_locale}.js")
1197 end
1198 end
1198 tags
1199 tags
1199 end
1200 end
1200 end
1201 end
1201 end
1202 end
1202
1203
1203 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1204 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1204 # Examples:
1205 # Examples:
1205 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1206 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1206 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1207 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1207 #
1208 #
1208 def stylesheet_link_tag(*sources)
1209 def stylesheet_link_tag(*sources)
1209 options = sources.last.is_a?(Hash) ? sources.pop : {}
1210 options = sources.last.is_a?(Hash) ? sources.pop : {}
1210 plugin = options.delete(:plugin)
1211 plugin = options.delete(:plugin)
1211 sources = sources.map do |source|
1212 sources = sources.map do |source|
1212 if plugin
1213 if plugin
1213 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1214 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1214 elsif current_theme && current_theme.stylesheets.include?(source)
1215 elsif current_theme && current_theme.stylesheets.include?(source)
1215 current_theme.stylesheet_path(source)
1216 current_theme.stylesheet_path(source)
1216 else
1217 else
1217 source
1218 source
1218 end
1219 end
1219 end
1220 end
1220 super *sources, options
1221 super *sources, options
1221 end
1222 end
1222
1223
1223 # Overrides Rails' image_tag with themes and plugins support.
1224 # Overrides Rails' image_tag with themes and plugins support.
1224 # Examples:
1225 # Examples:
1225 # image_tag('image.png') # => picks image.png from the current theme or defaults
1226 # image_tag('image.png') # => picks image.png from the current theme or defaults
1226 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1227 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1227 #
1228 #
1228 def image_tag(source, options={})
1229 def image_tag(source, options={})
1229 if plugin = options.delete(:plugin)
1230 if plugin = options.delete(:plugin)
1230 source = "/plugin_assets/#{plugin}/images/#{source}"
1231 source = "/plugin_assets/#{plugin}/images/#{source}"
1231 elsif current_theme && current_theme.images.include?(source)
1232 elsif current_theme && current_theme.images.include?(source)
1232 source = current_theme.image_path(source)
1233 source = current_theme.image_path(source)
1233 end
1234 end
1234 super source, options
1235 super source, options
1235 end
1236 end
1236
1237
1237 # Overrides Rails' javascript_include_tag with plugins support
1238 # Overrides Rails' javascript_include_tag with plugins support
1238 # Examples:
1239 # Examples:
1239 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1240 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1240 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1241 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1241 #
1242 #
1242 def javascript_include_tag(*sources)
1243 def javascript_include_tag(*sources)
1243 options = sources.last.is_a?(Hash) ? sources.pop : {}
1244 options = sources.last.is_a?(Hash) ? sources.pop : {}
1244 if plugin = options.delete(:plugin)
1245 if plugin = options.delete(:plugin)
1245 sources = sources.map do |source|
1246 sources = sources.map do |source|
1246 if plugin
1247 if plugin
1247 "/plugin_assets/#{plugin}/javascripts/#{source}"
1248 "/plugin_assets/#{plugin}/javascripts/#{source}"
1248 else
1249 else
1249 source
1250 source
1250 end
1251 end
1251 end
1252 end
1252 end
1253 end
1253 super *sources, options
1254 super *sources, options
1254 end
1255 end
1255
1256
1256 def sidebar_content?
1257 def sidebar_content?
1257 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1258 content_for?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1258 end
1259 end
1259
1260
1260 def view_layouts_base_sidebar_hook_response
1261 def view_layouts_base_sidebar_hook_response
1261 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1262 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1262 end
1263 end
1263
1264
1264 def email_delivery_enabled?
1265 def email_delivery_enabled?
1265 !!ActionMailer::Base.perform_deliveries
1266 !!ActionMailer::Base.perform_deliveries
1266 end
1267 end
1267
1268
1268 # Returns the avatar image tag for the given +user+ if avatars are enabled
1269 # Returns the avatar image tag for the given +user+ if avatars are enabled
1269 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1270 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1270 def avatar(user, options = { })
1271 def avatar(user, options = { })
1271 if Setting.gravatar_enabled?
1272 if Setting.gravatar_enabled?
1272 options.merge!(:default => Setting.gravatar_default)
1273 options.merge!(:default => Setting.gravatar_default)
1273 email = nil
1274 email = nil
1274 if user.respond_to?(:mail)
1275 if user.respond_to?(:mail)
1275 email = user.mail
1276 email = user.mail
1276 elsif user.to_s =~ %r{<(.+?)>}
1277 elsif user.to_s =~ %r{<(.+?)>}
1277 email = $1
1278 email = $1
1278 end
1279 end
1279 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1280 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1280 else
1281 else
1281 ''
1282 ''
1282 end
1283 end
1283 end
1284 end
1284
1285
1285 # Returns a link to edit user's avatar if avatars are enabled
1286 # Returns a link to edit user's avatar if avatars are enabled
1286 def avatar_edit_link(user, options={})
1287 def avatar_edit_link(user, options={})
1287 if Setting.gravatar_enabled?
1288 if Setting.gravatar_enabled?
1288 url = "https://gravatar.com"
1289 url = "https://gravatar.com"
1289 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1290 link_to avatar(user, {:title => l(:button_edit)}.merge(options)), url, :target => '_blank'
1290 end
1291 end
1291 end
1292 end
1292
1293
1293 def sanitize_anchor_name(anchor)
1294 def sanitize_anchor_name(anchor)
1294 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1295 anchor.gsub(%r{[^\s\-\p{Word}]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1295 end
1296 end
1296
1297
1297 # Returns the javascript tags that are included in the html layout head
1298 # Returns the javascript tags that are included in the html layout head
1298 def javascript_heads
1299 def javascript_heads
1299 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application', 'responsive')
1300 tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application', 'responsive')
1300 unless User.current.pref.warn_on_leaving_unsaved == '0'
1301 unless User.current.pref.warn_on_leaving_unsaved == '0'
1301 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1302 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1302 end
1303 end
1303 tags
1304 tags
1304 end
1305 end
1305
1306
1306 def favicon
1307 def favicon
1307 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1308 "<link rel='shortcut icon' href='#{favicon_path}' />".html_safe
1308 end
1309 end
1309
1310
1310 # Returns the path to the favicon
1311 # Returns the path to the favicon
1311 def favicon_path
1312 def favicon_path
1312 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1313 icon = (current_theme && current_theme.favicon?) ? current_theme.favicon_path : '/favicon.ico'
1313 image_path(icon)
1314 image_path(icon)
1314 end
1315 end
1315
1316
1316 # Returns the full URL to the favicon
1317 # Returns the full URL to the favicon
1317 def favicon_url
1318 def favicon_url
1318 # TODO: use #image_url introduced in Rails4
1319 # TODO: use #image_url introduced in Rails4
1319 path = favicon_path
1320 path = favicon_path
1320 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1321 base = url_for(:controller => 'welcome', :action => 'index', :only_path => false)
1321 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1322 base.sub(%r{/+$},'') + '/' + path.sub(%r{^/+},'')
1322 end
1323 end
1323
1324
1324 def robot_exclusion_tag
1325 def robot_exclusion_tag
1325 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1326 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1326 end
1327 end
1327
1328
1328 # Returns true if arg is expected in the API response
1329 # Returns true if arg is expected in the API response
1329 def include_in_api_response?(arg)
1330 def include_in_api_response?(arg)
1330 unless @included_in_api_response
1331 unless @included_in_api_response
1331 param = params[:include]
1332 param = params[:include]
1332 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1333 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1333 @included_in_api_response.collect!(&:strip)
1334 @included_in_api_response.collect!(&:strip)
1334 end
1335 end
1335 @included_in_api_response.include?(arg.to_s)
1336 @included_in_api_response.include?(arg.to_s)
1336 end
1337 end
1337
1338
1338 # Returns options or nil if nometa param or X-Redmine-Nometa header
1339 # Returns options or nil if nometa param or X-Redmine-Nometa header
1339 # was set in the request
1340 # was set in the request
1340 def api_meta(options)
1341 def api_meta(options)
1341 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1342 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1342 # compatibility mode for activeresource clients that raise
1343 # compatibility mode for activeresource clients that raise
1343 # an error when deserializing an array with attributes
1344 # an error when deserializing an array with attributes
1344 nil
1345 nil
1345 else
1346 else
1346 options
1347 options
1347 end
1348 end
1348 end
1349 end
1349
1350
1350 def generate_csv(&block)
1351 def generate_csv(&block)
1351 decimal_separator = l(:general_csv_decimal_separator)
1352 decimal_separator = l(:general_csv_decimal_separator)
1352 encoding = l(:general_csv_encoding)
1353 encoding = l(:general_csv_encoding)
1353 end
1354 end
1354
1355
1355 private
1356 private
1356
1357
1357 def wiki_helper
1358 def wiki_helper
1358 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1359 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1359 extend helper
1360 extend helper
1360 return self
1361 return self
1361 end
1362 end
1362
1363
1363 def link_to_content_update(text, url_params = {}, html_options = {})
1364 def link_to_content_update(text, url_params = {}, html_options = {})
1364 link_to(text, url_params, html_options)
1365 link_to(text, url_params, html_options)
1365 end
1366 end
1366 end
1367 end
@@ -1,284 +1,292
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class CustomField < ActiveRecord::Base
18 class CustomField < ActiveRecord::Base
19 include Redmine::SubclassFactory
19 include Redmine::SubclassFactory
20
20
21 has_many :enumerations,
21 has_many :enumerations,
22 lambda { order(:position) },
22 lambda { order(:position) },
23 :class_name => 'CustomFieldEnumeration',
23 :class_name => 'CustomFieldEnumeration',
24 :dependent => :delete_all
24 :dependent => :delete_all
25 has_many :custom_values, :dependent => :delete_all
25 has_many :custom_values, :dependent => :delete_all
26 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "custom_field_id"
26 has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}custom_fields_roles#{table_name_suffix}", :foreign_key => "custom_field_id"
27 acts_as_positioned
27 acts_as_positioned
28 serialize :possible_values
28 serialize :possible_values
29 store :format_store
29 store :format_store
30
30
31 validates_presence_of :name, :field_format
31 validates_presence_of :name, :field_format
32 validates_uniqueness_of :name, :scope => :type
32 validates_uniqueness_of :name, :scope => :type
33 validates_length_of :name, :maximum => 30
33 validates_length_of :name, :maximum => 30
34 validates_inclusion_of :field_format, :in => Proc.new { Redmine::FieldFormat.available_formats }
34 validates_inclusion_of :field_format, :in => Proc.new { Redmine::FieldFormat.available_formats }
35 validate :validate_custom_field
35 validate :validate_custom_field
36 attr_protected :id
36 attr_protected :id
37
37
38 before_validation :set_searchable
38 before_validation :set_searchable
39 before_save do |field|
39 before_save do |field|
40 field.format.before_custom_field_save(field)
40 field.format.before_custom_field_save(field)
41 end
41 end
42 after_save :handle_multiplicity_change
42 after_save :handle_multiplicity_change
43 after_save do |field|
43 after_save do |field|
44 if field.visible_changed? && field.visible
44 if field.visible_changed? && field.visible
45 field.roles.clear
45 field.roles.clear
46 end
46 end
47 end
47 end
48
48
49 scope :sorted, lambda { order(:position) }
49 scope :sorted, lambda { order(:position) }
50 scope :visible, lambda {|*args|
50 scope :visible, lambda {|*args|
51 user = args.shift || User.current
51 user = args.shift || User.current
52 if user.admin?
52 if user.admin?
53 # nop
53 # nop
54 elsif user.memberships.any?
54 elsif user.memberships.any?
55 where("#{table_name}.visible = ? OR #{table_name}.id IN (SELECT DISTINCT cfr.custom_field_id FROM #{Member.table_name} m" +
55 where("#{table_name}.visible = ? OR #{table_name}.id IN (SELECT DISTINCT cfr.custom_field_id FROM #{Member.table_name} m" +
56 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
56 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
57 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
57 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
58 " WHERE m.user_id = ?)",
58 " WHERE m.user_id = ?)",
59 true, user.id)
59 true, user.id)
60 else
60 else
61 where(:visible => true)
61 where(:visible => true)
62 end
62 end
63 }
63 }
64
64
65 def visible_by?(project, user=User.current)
65 def visible_by?(project, user=User.current)
66 visible? || user.admin?
66 visible? || user.admin?
67 end
67 end
68
68
69 def format
69 def format
70 @format ||= Redmine::FieldFormat.find(field_format)
70 @format ||= Redmine::FieldFormat.find(field_format)
71 end
71 end
72
72
73 def field_format=(arg)
73 def field_format=(arg)
74 # cannot change format of a saved custom field
74 # cannot change format of a saved custom field
75 if new_record?
75 if new_record?
76 @format = nil
76 @format = nil
77 super
77 super
78 end
78 end
79 end
79 end
80
80
81 def set_searchable
81 def set_searchable
82 # make sure these fields are not searchable
82 # make sure these fields are not searchable
83 self.searchable = false unless format.class.searchable_supported
83 self.searchable = false unless format.class.searchable_supported
84 # make sure only these fields can have multiple values
84 # make sure only these fields can have multiple values
85 self.multiple = false unless format.class.multiple_supported
85 self.multiple = false unless format.class.multiple_supported
86 true
86 true
87 end
87 end
88
88
89 def validate_custom_field
89 def validate_custom_field
90 format.validate_custom_field(self).each do |attribute, message|
90 format.validate_custom_field(self).each do |attribute, message|
91 errors.add attribute, message
91 errors.add attribute, message
92 end
92 end
93
93
94 if regexp.present?
94 if regexp.present?
95 begin
95 begin
96 Regexp.new(regexp)
96 Regexp.new(regexp)
97 rescue
97 rescue
98 errors.add(:regexp, :invalid)
98 errors.add(:regexp, :invalid)
99 end
99 end
100 end
100 end
101
101
102 if default_value.present?
102 if default_value.present?
103 validate_field_value(default_value).each do |message|
103 validate_field_value(default_value).each do |message|
104 errors.add :default_value, message
104 errors.add :default_value, message
105 end
105 end
106 end
106 end
107 end
107 end
108
108
109 def possible_custom_value_options(custom_value)
109 def possible_custom_value_options(custom_value)
110 format.possible_custom_value_options(custom_value)
110 format.possible_custom_value_options(custom_value)
111 end
111 end
112
112
113 def possible_values_options(object=nil)
113 def possible_values_options(object=nil)
114 if object.is_a?(Array)
114 if object.is_a?(Array)
115 object.map {|o| format.possible_values_options(self, o)}.reduce(:&) || []
115 object.map {|o| format.possible_values_options(self, o)}.reduce(:&) || []
116 else
116 else
117 format.possible_values_options(self, object) || []
117 format.possible_values_options(self, object) || []
118 end
118 end
119 end
119 end
120
120
121 def possible_values
121 def possible_values
122 values = read_attribute(:possible_values)
122 values = read_attribute(:possible_values)
123 if values.is_a?(Array)
123 if values.is_a?(Array)
124 values.each do |value|
124 values.each do |value|
125 value.to_s.force_encoding('UTF-8')
125 value.to_s.force_encoding('UTF-8')
126 end
126 end
127 values
127 values
128 else
128 else
129 []
129 []
130 end
130 end
131 end
131 end
132
132
133 # Makes possible_values accept a multiline string
133 # Makes possible_values accept a multiline string
134 def possible_values=(arg)
134 def possible_values=(arg)
135 if arg.is_a?(Array)
135 if arg.is_a?(Array)
136 values = arg.compact.map {|a| a.to_s.strip}.reject(&:blank?)
136 values = arg.compact.map {|a| a.to_s.strip}.reject(&:blank?)
137 write_attribute(:possible_values, values)
137 write_attribute(:possible_values, values)
138 else
138 else
139 self.possible_values = arg.to_s.split(/[\n\r]+/)
139 self.possible_values = arg.to_s.split(/[\n\r]+/)
140 end
140 end
141 end
141 end
142
142
143 def cast_value(value)
143 def cast_value(value)
144 format.cast_value(self, value)
144 format.cast_value(self, value)
145 end
145 end
146
146
147 def value_from_keyword(keyword, customized)
147 def value_from_keyword(keyword, customized)
148 format.value_from_keyword(self, keyword, customized)
148 format.value_from_keyword(self, keyword, customized)
149 end
149 end
150
150
151 # Returns the options hash used to build a query filter for the field
151 # Returns the options hash used to build a query filter for the field
152 def query_filter_options(query)
152 def query_filter_options(query)
153 format.query_filter_options(self, query)
153 format.query_filter_options(self, query)
154 end
154 end
155
155
156 def totalable?
156 def totalable?
157 format.totalable_supported
157 format.totalable_supported
158 end
158 end
159
159
160 # Returns a ORDER BY clause that can used to sort customized
160 # Returns a ORDER BY clause that can used to sort customized
161 # objects by their value of the custom field.
161 # objects by their value of the custom field.
162 # Returns nil if the custom field can not be used for sorting.
162 # Returns nil if the custom field can not be used for sorting.
163 def order_statement
163 def order_statement
164 return nil if multiple?
164 return nil if multiple?
165 format.order_statement(self)
165 format.order_statement(self)
166 end
166 end
167
167
168 # Returns a GROUP BY clause that can used to group by custom value
168 # Returns a GROUP BY clause that can used to group by custom value
169 # Returns nil if the custom field can not be used for grouping.
169 # Returns nil if the custom field can not be used for grouping.
170 def group_statement
170 def group_statement
171 return nil if multiple?
171 return nil if multiple?
172 format.group_statement(self)
172 format.group_statement(self)
173 end
173 end
174
174
175 def join_for_order_statement
175 def join_for_order_statement
176 format.join_for_order_statement(self)
176 format.join_for_order_statement(self)
177 end
177 end
178
178
179 def visibility_by_project_condition(project_key=nil, user=User.current, id_column=nil)
179 def visibility_by_project_condition(project_key=nil, user=User.current, id_column=nil)
180 if visible? || user.admin?
180 if visible? || user.admin?
181 "1=1"
181 "1=1"
182 elsif user.anonymous?
182 elsif user.anonymous?
183 "1=0"
183 "1=0"
184 else
184 else
185 project_key ||= "#{self.class.customized_class.table_name}.project_id"
185 project_key ||= "#{self.class.customized_class.table_name}.project_id"
186 id_column ||= id
186 id_column ||= id
187 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
187 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
188 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
188 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
189 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
189 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
190 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id_column})"
190 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id_column})"
191 end
191 end
192 end
192 end
193
193
194 def self.visibility_condition
194 def self.visibility_condition
195 if user.admin?
195 if user.admin?
196 "1=1"
196 "1=1"
197 elsif user.anonymous?
197 elsif user.anonymous?
198 "#{table_name}.visible"
198 "#{table_name}.visible"
199 else
199 else
200 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
200 "#{project_key} IN (SELECT DISTINCT m.project_id FROM #{Member.table_name} m" +
201 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
201 " INNER JOIN #{MemberRole.table_name} mr ON mr.member_id = m.id" +
202 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
202 " INNER JOIN #{table_name_prefix}custom_fields_roles#{table_name_suffix} cfr ON cfr.role_id = mr.role_id" +
203 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id})"
203 " WHERE m.user_id = #{user.id} AND cfr.custom_field_id = #{id})"
204 end
204 end
205 end
205 end
206
206
207 def <=>(field)
207 def <=>(field)
208 position <=> field.position
208 position <=> field.position
209 end
209 end
210
210
211 # Returns the class that values represent
211 # Returns the class that values represent
212 def value_class
212 def value_class
213 format.target_class if format.respond_to?(:target_class)
213 format.target_class if format.respond_to?(:target_class)
214 end
214 end
215
215
216 def self.customized_class
216 def self.customized_class
217 self.name =~ /^(.+)CustomField$/
217 self.name =~ /^(.+)CustomField$/
218 $1.constantize rescue nil
218 $1.constantize rescue nil
219 end
219 end
220
220
221 # to move in project_custom_field
221 # to move in project_custom_field
222 def self.for_all
222 def self.for_all
223 where(:is_for_all => true).order('position').to_a
223 where(:is_for_all => true).order('position').to_a
224 end
224 end
225
225
226 def type_name
226 def type_name
227 nil
227 nil
228 end
228 end
229
229
230 # Returns the error messages for the given value
230 # Returns the error messages for the given value
231 # or an empty array if value is a valid value for the custom field
231 # or an empty array if value is a valid value for the custom field
232 def validate_custom_value(custom_value)
232 def validate_custom_value(custom_value)
233 value = custom_value.value
233 value = custom_value.value
234 errs = []
234 errs = []
235 if value.is_a?(Array)
235 if value.is_a?(Array)
236 if !multiple?
236 if !multiple?
237 errs << ::I18n.t('activerecord.errors.messages.invalid')
237 errs << ::I18n.t('activerecord.errors.messages.invalid')
238 end
238 end
239 if is_required? && value.detect(&:present?).nil?
239 if is_required? && value.detect(&:present?).nil?
240 errs << ::I18n.t('activerecord.errors.messages.blank')
240 errs << ::I18n.t('activerecord.errors.messages.blank')
241 end
241 end
242 else
242 else
243 if is_required? && value.blank?
243 if is_required? && value.blank?
244 errs << ::I18n.t('activerecord.errors.messages.blank')
244 errs << ::I18n.t('activerecord.errors.messages.blank')
245 end
245 end
246 end
246 end
247 errs += format.validate_custom_value(custom_value)
247 errs += format.validate_custom_value(custom_value)
248 errs
248 errs
249 end
249 end
250
250
251 # Returns the error messages for the default custom field value
251 # Returns the error messages for the default custom field value
252 def validate_field_value(value)
252 def validate_field_value(value)
253 validate_custom_value(CustomFieldValue.new(:custom_field => self, :value => value))
253 validate_custom_value(CustomFieldValue.new(:custom_field => self, :value => value))
254 end
254 end
255
255
256 # Returns true if value is a valid value for the custom field
256 # Returns true if value is a valid value for the custom field
257 def valid_field_value?(value)
257 def valid_field_value?(value)
258 validate_field_value(value).empty?
258 validate_field_value(value).empty?
259 end
259 end
260
260
261 def format_in?(*args)
261 def format_in?(*args)
262 args.include?(field_format)
262 args.include?(field_format)
263 end
263 end
264
264
265 def self.human_attribute_name(attribute_key_name, *args)
266 attr_name = attribute_key_name.to_s
267 if attr_name == 'url_pattern'
268 attr_name = "url"
269 end
270 super(attr_name, *args)
271 end
272
265 protected
273 protected
266
274
267 # Removes multiple values for the custom field after setting the multiple attribute to false
275 # Removes multiple values for the custom field after setting the multiple attribute to false
268 # We kepp the value with the highest id for each customized object
276 # We kepp the value with the highest id for each customized object
269 def handle_multiplicity_change
277 def handle_multiplicity_change
270 if !new_record? && multiple_was && !multiple
278 if !new_record? && multiple_was && !multiple
271 ids = custom_values.
279 ids = custom_values.
272 where("EXISTS(SELECT 1 FROM #{CustomValue.table_name} cve WHERE cve.custom_field_id = #{CustomValue.table_name}.custom_field_id" +
280 where("EXISTS(SELECT 1 FROM #{CustomValue.table_name} cve WHERE cve.custom_field_id = #{CustomValue.table_name}.custom_field_id" +
273 " AND cve.customized_type = #{CustomValue.table_name}.customized_type AND cve.customized_id = #{CustomValue.table_name}.customized_id" +
281 " AND cve.customized_type = #{CustomValue.table_name}.customized_type AND cve.customized_id = #{CustomValue.table_name}.customized_id" +
274 " AND cve.id > #{CustomValue.table_name}.id)").
282 " AND cve.id > #{CustomValue.table_name}.id)").
275 pluck(:id)
283 pluck(:id)
276
284
277 if ids.any?
285 if ids.any?
278 custom_values.where(:id => ids).delete_all
286 custom_values.where(:id => ids).delete_all
279 end
287 end
280 end
288 end
281 end
289 end
282 end
290 end
283
291
284 require_dependency 'redmine/field_format'
292 require_dependency 'redmine/field_format'
@@ -1,109 +1,109
1 <div class="contextual">
1 <div class="contextual">
2 <% if User.current.allowed_to?(:add_subprojects, @project) %>
2 <% if User.current.allowed_to?(:add_subprojects, @project) %>
3 <%= link_to l(:label_subproject_new), new_project_path(:parent_id => @project), :class => 'icon icon-add' %>
3 <%= link_to l(:label_subproject_new), new_project_path(:parent_id => @project), :class => 'icon icon-add' %>
4 <% end %>
4 <% end %>
5 <% if User.current.allowed_to?(:close_project, @project) %>
5 <% if User.current.allowed_to?(:close_project, @project) %>
6 <% if @project.active? %>
6 <% if @project.active? %>
7 <%= link_to l(:button_close), close_project_path(@project), :data => {:confirm => l(:text_are_you_sure)}, :method => :post, :class => 'icon icon-lock' %>
7 <%= link_to l(:button_close), close_project_path(@project), :data => {:confirm => l(:text_are_you_sure)}, :method => :post, :class => 'icon icon-lock' %>
8 <% else %>
8 <% else %>
9 <%= link_to l(:button_reopen), reopen_project_path(@project), :data => {:confirm => l(:text_are_you_sure)}, :method => :post, :class => 'icon icon-unlock' %>
9 <%= link_to l(:button_reopen), reopen_project_path(@project), :data => {:confirm => l(:text_are_you_sure)}, :method => :post, :class => 'icon icon-unlock' %>
10 <% end %>
10 <% end %>
11 <% end %>
11 <% end %>
12 </div>
12 </div>
13
13
14 <h2><%=l(:label_overview)%></h2>
14 <h2><%=l(:label_overview)%></h2>
15
15
16 <% unless @project.active? %>
16 <% unless @project.active? %>
17 <p class="warning"><span class="icon icon-lock"><%= l(:text_project_closed) %></span></p>
17 <p class="warning"><span class="icon icon-lock"><%= l(:text_project_closed) %></span></p>
18 <% end %>
18 <% end %>
19
19
20 <div class="splitcontentleft">
20 <div class="splitcontentleft">
21 <% if @project.description.present? %>
21 <% if @project.description.present? %>
22 <div class="wiki">
22 <div class="wiki">
23 <%= textilizable @project.description %>
23 <%= textilizable @project.description %>
24 </div>
24 </div>
25 <% end %>
25 <% end %>
26 <% if @project.homepage.present? || @subprojects.any? || @project.visible_custom_field_values.any?(&:present?) %>
26 <% if @project.homepage.present? || @subprojects.any? || @project.visible_custom_field_values.any?(&:present?) %>
27 <ul>
27 <ul>
28 <% unless @project.homepage.blank? %>
28 <% unless @project.homepage.blank? %>
29 <li><span class="label"><%=l(:field_homepage)%>:</span> <%= link_to @project.homepage, @project.homepage %></li>
29 <li><span class="label"><%=l(:field_homepage)%>:</span> <%= link_to_if uri_with_safe_scheme?(@project.homepage), @project.homepage, @project.homepage %></li>
30 <% end %>
30 <% end %>
31 <% if @subprojects.any? %>
31 <% if @subprojects.any? %>
32 <li><span class="label"><%=l(:label_subproject_plural)%>:</span>
32 <li><span class="label"><%=l(:label_subproject_plural)%>:</span>
33 <%= @subprojects.collect{|p| link_to p, project_path(p)}.join(", ").html_safe %></li>
33 <%= @subprojects.collect{|p| link_to p, project_path(p)}.join(", ").html_safe %></li>
34 <% end %>
34 <% end %>
35 <% render_custom_field_values(@project) do |custom_field, formatted| %>
35 <% render_custom_field_values(@project) do |custom_field, formatted| %>
36 <li><span class="label"><%= custom_field.name %>:</span> <%= formatted %></li>
36 <li><span class="label"><%= custom_field.name %>:</span> <%= formatted %></li>
37 <% end %>
37 <% end %>
38 </ul>
38 </ul>
39 <% end %>
39 <% end %>
40
40
41 <% if User.current.allowed_to?(:view_issues, @project) %>
41 <% if User.current.allowed_to?(:view_issues, @project) %>
42 <div class="issues box">
42 <div class="issues box">
43 <h3><%=l(:label_issue_tracking)%></h3>
43 <h3><%=l(:label_issue_tracking)%></h3>
44 <% if @trackers.present? %>
44 <% if @trackers.present? %>
45 <table class="list issue-report">
45 <table class="list issue-report">
46 <thead>
46 <thead>
47 <tr>
47 <tr>
48 <th></th>
48 <th></th>
49 <th><%=l(:label_open_issues_plural)%></th>
49 <th><%=l(:label_open_issues_plural)%></th>
50 <th><%=l(:label_closed_issues_plural)%></th>
50 <th><%=l(:label_closed_issues_plural)%></th>
51 <th><%=l(:label_total)%></th>
51 <th><%=l(:label_total)%></th>
52 </tr>
52 </tr>
53 </thead>
53 </thead>
54 <tbody>
54 <tbody>
55 <% @trackers.each do |tracker| %>
55 <% @trackers.each do |tracker| %>
56 <tr class="<%= cycle("odd", "even") %>">
56 <tr class="<%= cycle("odd", "even") %>">
57 <td class="name">
57 <td class="name">
58 <%= link_to tracker.name, project_issues_path(@project, :set_filter => 1, :tracker_id => tracker.id) %>
58 <%= link_to tracker.name, project_issues_path(@project, :set_filter => 1, :tracker_id => tracker.id) %>
59 </td>
59 </td>
60 <td>
60 <td>
61 <%= link_to @open_issues_by_tracker[tracker].to_i, project_issues_path(@project, :set_filter => 1, :tracker_id => tracker.id) %>
61 <%= link_to @open_issues_by_tracker[tracker].to_i, project_issues_path(@project, :set_filter => 1, :tracker_id => tracker.id) %>
62 </td>
62 </td>
63 <td>
63 <td>
64 <%= link_to (@total_issues_by_tracker[tracker].to_i - @open_issues_by_tracker[tracker].to_i), project_issues_path(@project, :set_filter => 1, :tracker_id => tracker.id, :status_id => 'c') %>
64 <%= link_to (@total_issues_by_tracker[tracker].to_i - @open_issues_by_tracker[tracker].to_i), project_issues_path(@project, :set_filter => 1, :tracker_id => tracker.id, :status_id => 'c') %>
65 </td>
65 </td>
66 <td>
66 <td>
67 <%= link_to @total_issues_by_tracker[tracker].to_i, project_issues_path(@project, :set_filter => 1, :tracker_id => tracker.id, :status_id => '*') %>
67 <%= link_to @total_issues_by_tracker[tracker].to_i, project_issues_path(@project, :set_filter => 1, :tracker_id => tracker.id, :status_id => '*') %>
68 </td>
68 </td>
69 </tr>
69 </tr>
70 <% end %>
70 <% end %>
71 </tbody>
71 </tbody>
72 </table>
72 </table>
73 <% end %>
73 <% end %>
74 <p>
74 <p>
75 <%= link_to l(:label_issue_view_all), project_issues_path(@project, :set_filter => 1) %>
75 <%= link_to l(:label_issue_view_all), project_issues_path(@project, :set_filter => 1) %>
76 <% if User.current.allowed_to?(:view_calendar, @project, :global => true) %>
76 <% if User.current.allowed_to?(:view_calendar, @project, :global => true) %>
77 | <%= link_to l(:label_calendar), project_calendar_path(@project) %>
77 | <%= link_to l(:label_calendar), project_calendar_path(@project) %>
78 <% end %>
78 <% end %>
79 <% if User.current.allowed_to?(:view_gantt, @project, :global => true) %>
79 <% if User.current.allowed_to?(:view_gantt, @project, :global => true) %>
80 | <%= link_to l(:label_gantt), project_gantt_path(@project) %>
80 | <%= link_to l(:label_gantt), project_gantt_path(@project) %>
81 <% end %>
81 <% end %>
82 </p>
82 </p>
83 </div>
83 </div>
84 <% end %>
84 <% end %>
85 <%= call_hook(:view_projects_show_left, :project => @project) %>
85 <%= call_hook(:view_projects_show_left, :project => @project) %>
86 </div>
86 </div>
87
87
88 <div class="splitcontentright">
88 <div class="splitcontentright">
89 <%= render :partial => 'members_box' %>
89 <%= render :partial => 'members_box' %>
90
90
91 <% if @news.any? && authorize_for('news', 'index') %>
91 <% if @news.any? && authorize_for('news', 'index') %>
92 <div class="news box">
92 <div class="news box">
93 <h3><%=l(:label_news_latest)%></h3>
93 <h3><%=l(:label_news_latest)%></h3>
94 <%= render :partial => 'news/news', :collection => @news %>
94 <%= render :partial => 'news/news', :collection => @news %>
95 <p><%= link_to l(:label_news_view_all), project_news_index_path(@project) %></p>
95 <p><%= link_to l(:label_news_view_all), project_news_index_path(@project) %></p>
96 </div>
96 </div>
97 <% end %>
97 <% end %>
98 <%= call_hook(:view_projects_show_right, :project => @project) %>
98 <%= call_hook(:view_projects_show_right, :project => @project) %>
99 </div>
99 </div>
100
100
101 <% content_for :sidebar do %>
101 <% content_for :sidebar do %>
102 <%= render :partial => 'projects/sidebar' %>
102 <%= render :partial => 'projects/sidebar' %>
103 <% end %>
103 <% end %>
104
104
105 <% content_for :header_tags do %>
105 <% content_for :header_tags do %>
106 <%= auto_discovery_link_tag(:atom, {:controller => 'activities', :action => 'index', :id => @project, :format => 'atom', :key => User.current.rss_key}) %>
106 <%= auto_discovery_link_tag(:atom, {:controller => 'activities', :action => 'index', :id => @project, :format => 'atom', :key => User.current.rss_key}) %>
107 <% end %>
107 <% end %>
108
108
109 <% html_title(l(:label_overview)) -%>
109 <% html_title(l(:label_overview)) -%>
@@ -1,1208 +1,1211
1 # vim:ts=4:sw=4:
1 # vim:ts=4:sw=4:
2 # = RedCloth - Textile and Markdown Hybrid for Ruby
2 # = RedCloth - Textile and Markdown Hybrid for Ruby
3 #
3 #
4 # Homepage:: http://whytheluckystiff.net/ruby/redcloth/
4 # Homepage:: http://whytheluckystiff.net/ruby/redcloth/
5 # Author:: why the lucky stiff (http://whytheluckystiff.net/)
5 # Author:: why the lucky stiff (http://whytheluckystiff.net/)
6 # Copyright:: (cc) 2004 why the lucky stiff (and his puppet organizations.)
6 # Copyright:: (cc) 2004 why the lucky stiff (and his puppet organizations.)
7 # License:: BSD
7 # License:: BSD
8 #
8 #
9 # (see http://hobix.com/textile/ for a Textile Reference.)
9 # (see http://hobix.com/textile/ for a Textile Reference.)
10 #
10 #
11 # Based on (and also inspired by) both:
11 # Based on (and also inspired by) both:
12 #
12 #
13 # PyTextile: http://diveintomark.org/projects/textile/textile.py.txt
13 # PyTextile: http://diveintomark.org/projects/textile/textile.py.txt
14 # Textism for PHP: http://www.textism.com/tools/textile/
14 # Textism for PHP: http://www.textism.com/tools/textile/
15 #
15 #
16 #
16 #
17
17
18 # = RedCloth
18 # = RedCloth
19 #
19 #
20 # RedCloth is a Ruby library for converting Textile and/or Markdown
20 # RedCloth is a Ruby library for converting Textile and/or Markdown
21 # into HTML. You can use either format, intermingled or separately.
21 # into HTML. You can use either format, intermingled or separately.
22 # You can also extend RedCloth to honor your own custom text stylings.
22 # You can also extend RedCloth to honor your own custom text stylings.
23 #
23 #
24 # RedCloth users are encouraged to use Textile if they are generating
24 # RedCloth users are encouraged to use Textile if they are generating
25 # HTML and to use Markdown if others will be viewing the plain text.
25 # HTML and to use Markdown if others will be viewing the plain text.
26 #
26 #
27 # == What is Textile?
27 # == What is Textile?
28 #
28 #
29 # Textile is a simple formatting style for text
29 # Textile is a simple formatting style for text
30 # documents, loosely based on some HTML conventions.
30 # documents, loosely based on some HTML conventions.
31 #
31 #
32 # == Sample Textile Text
32 # == Sample Textile Text
33 #
33 #
34 # h2. This is a title
34 # h2. This is a title
35 #
35 #
36 # h3. This is a subhead
36 # h3. This is a subhead
37 #
37 #
38 # This is a bit of paragraph.
38 # This is a bit of paragraph.
39 #
39 #
40 # bq. This is a blockquote.
40 # bq. This is a blockquote.
41 #
41 #
42 # = Writing Textile
42 # = Writing Textile
43 #
43 #
44 # A Textile document consists of paragraphs. Paragraphs
44 # A Textile document consists of paragraphs. Paragraphs
45 # can be specially formatted by adding a small instruction
45 # can be specially formatted by adding a small instruction
46 # to the beginning of the paragraph.
46 # to the beginning of the paragraph.
47 #
47 #
48 # h[n]. Header of size [n].
48 # h[n]. Header of size [n].
49 # bq. Blockquote.
49 # bq. Blockquote.
50 # # Numeric list.
50 # # Numeric list.
51 # * Bulleted list.
51 # * Bulleted list.
52 #
52 #
53 # == Quick Phrase Modifiers
53 # == Quick Phrase Modifiers
54 #
54 #
55 # Quick phrase modifiers are also included, to allow formatting
55 # Quick phrase modifiers are also included, to allow formatting
56 # of small portions of text within a paragraph.
56 # of small portions of text within a paragraph.
57 #
57 #
58 # \_emphasis\_
58 # \_emphasis\_
59 # \_\_italicized\_\_
59 # \_\_italicized\_\_
60 # \*strong\*
60 # \*strong\*
61 # \*\*bold\*\*
61 # \*\*bold\*\*
62 # ??citation??
62 # ??citation??
63 # -deleted text-
63 # -deleted text-
64 # +inserted text+
64 # +inserted text+
65 # ^superscript^
65 # ^superscript^
66 # ~subscript~
66 # ~subscript~
67 # @code@
67 # @code@
68 # %(classname)span%
68 # %(classname)span%
69 #
69 #
70 # ==notextile== (leave text alone)
70 # ==notextile== (leave text alone)
71 #
71 #
72 # == Links
72 # == Links
73 #
73 #
74 # To make a hypertext link, put the link text in "quotation
74 # To make a hypertext link, put the link text in "quotation
75 # marks" followed immediately by a colon and the URL of the link.
75 # marks" followed immediately by a colon and the URL of the link.
76 #
76 #
77 # Optional: text in (parentheses) following the link text,
77 # Optional: text in (parentheses) following the link text,
78 # but before the closing quotation mark, will become a Title
78 # but before the closing quotation mark, will become a Title
79 # attribute for the link, visible as a tool tip when a cursor is above it.
79 # attribute for the link, visible as a tool tip when a cursor is above it.
80 #
80 #
81 # Example:
81 # Example:
82 #
82 #
83 # "This is a link (This is a title) ":http://www.textism.com
83 # "This is a link (This is a title) ":http://www.textism.com
84 #
84 #
85 # Will become:
85 # Will become:
86 #
86 #
87 # <a href="http://www.textism.com" title="This is a title">This is a link</a>
87 # <a href="http://www.textism.com" title="This is a title">This is a link</a>
88 #
88 #
89 # == Images
89 # == Images
90 #
90 #
91 # To insert an image, put the URL for the image inside exclamation marks.
91 # To insert an image, put the URL for the image inside exclamation marks.
92 #
92 #
93 # Optional: text that immediately follows the URL in (parentheses) will
93 # Optional: text that immediately follows the URL in (parentheses) will
94 # be used as the Alt text for the image. Images on the web should always
94 # be used as the Alt text for the image. Images on the web should always
95 # have descriptive Alt text for the benefit of readers using non-graphical
95 # have descriptive Alt text for the benefit of readers using non-graphical
96 # browsers.
96 # browsers.
97 #
97 #
98 # Optional: place a colon followed by a URL immediately after the
98 # Optional: place a colon followed by a URL immediately after the
99 # closing ! to make the image into a link.
99 # closing ! to make the image into a link.
100 #
100 #
101 # Example:
101 # Example:
102 #
102 #
103 # !http://www.textism.com/common/textist.gif(Textist)!
103 # !http://www.textism.com/common/textist.gif(Textist)!
104 #
104 #
105 # Will become:
105 # Will become:
106 #
106 #
107 # <img src="http://www.textism.com/common/textist.gif" alt="Textist" />
107 # <img src="http://www.textism.com/common/textist.gif" alt="Textist" />
108 #
108 #
109 # With a link:
109 # With a link:
110 #
110 #
111 # !/common/textist.gif(Textist)!:http://textism.com
111 # !/common/textist.gif(Textist)!:http://textism.com
112 #
112 #
113 # Will become:
113 # Will become:
114 #
114 #
115 # <a href="http://textism.com"><img src="/common/textist.gif" alt="Textist" /></a>
115 # <a href="http://textism.com"><img src="/common/textist.gif" alt="Textist" /></a>
116 #
116 #
117 # == Defining Acronyms
117 # == Defining Acronyms
118 #
118 #
119 # HTML allows authors to define acronyms via the tag. The definition appears as a
119 # HTML allows authors to define acronyms via the tag. The definition appears as a
120 # tool tip when a cursor hovers over the acronym. A crucial aid to clear writing,
120 # tool tip when a cursor hovers over the acronym. A crucial aid to clear writing,
121 # this should be used at least once for each acronym in documents where they appear.
121 # this should be used at least once for each acronym in documents where they appear.
122 #
122 #
123 # To quickly define an acronym in Textile, place the full text in (parentheses)
123 # To quickly define an acronym in Textile, place the full text in (parentheses)
124 # immediately following the acronym.
124 # immediately following the acronym.
125 #
125 #
126 # Example:
126 # Example:
127 #
127 #
128 # ACLU(American Civil Liberties Union)
128 # ACLU(American Civil Liberties Union)
129 #
129 #
130 # Will become:
130 # Will become:
131 #
131 #
132 # <abbr title="American Civil Liberties Union">ACLU</abbr>
132 # <abbr title="American Civil Liberties Union">ACLU</abbr>
133 #
133 #
134 # == Adding Tables
134 # == Adding Tables
135 #
135 #
136 # In Textile, simple tables can be added by separating each column by
136 # In Textile, simple tables can be added by separating each column by
137 # a pipe.
137 # a pipe.
138 #
138 #
139 # |a|simple|table|row|
139 # |a|simple|table|row|
140 # |And|Another|table|row|
140 # |And|Another|table|row|
141 #
141 #
142 # Attributes are defined by style definitions in parentheses.
142 # Attributes are defined by style definitions in parentheses.
143 #
143 #
144 # table(border:1px solid black).
144 # table(border:1px solid black).
145 # (background:#ddd;color:red). |{}| | | |
145 # (background:#ddd;color:red). |{}| | | |
146 #
146 #
147 # == Using RedCloth
147 # == Using RedCloth
148 #
148 #
149 # RedCloth is simply an extension of the String class, which can handle
149 # RedCloth is simply an extension of the String class, which can handle
150 # Textile formatting. Use it like a String and output HTML with its
150 # Textile formatting. Use it like a String and output HTML with its
151 # RedCloth#to_html method.
151 # RedCloth#to_html method.
152 #
152 #
153 # doc = RedCloth.new "
153 # doc = RedCloth.new "
154 #
154 #
155 # h2. Test document
155 # h2. Test document
156 #
156 #
157 # Just a simple test."
157 # Just a simple test."
158 #
158 #
159 # puts doc.to_html
159 # puts doc.to_html
160 #
160 #
161 # By default, RedCloth uses both Textile and Markdown formatting, with
161 # By default, RedCloth uses both Textile and Markdown formatting, with
162 # Textile formatting taking precedence. If you want to turn off Markdown
162 # Textile formatting taking precedence. If you want to turn off Markdown
163 # formatting, to boost speed and limit the processor:
163 # formatting, to boost speed and limit the processor:
164 #
164 #
165 # class RedCloth::Textile.new( str )
165 # class RedCloth::Textile.new( str )
166
166
167 class RedCloth3 < String
167 class RedCloth3 < String
168 include Redmine::Helpers::URL
168
169
169 VERSION = '3.0.4'
170 VERSION = '3.0.4'
170 DEFAULT_RULES = [:textile, :markdown]
171 DEFAULT_RULES = [:textile, :markdown]
171
172
172 #
173 #
173 # Two accessor for setting security restrictions.
174 # Two accessor for setting security restrictions.
174 #
175 #
175 # This is a nice thing if you're using RedCloth for
176 # This is a nice thing if you're using RedCloth for
176 # formatting in public places (e.g. Wikis) where you
177 # formatting in public places (e.g. Wikis) where you
177 # don't want users to abuse HTML for bad things.
178 # don't want users to abuse HTML for bad things.
178 #
179 #
179 # If +:filter_html+ is set, HTML which wasn't
180 # If +:filter_html+ is set, HTML which wasn't
180 # created by the Textile processor will be escaped.
181 # created by the Textile processor will be escaped.
181 #
182 #
182 # If +:filter_styles+ is set, it will also disable
183 # If +:filter_styles+ is set, it will also disable
183 # the style markup specifier. ('{color: red}')
184 # the style markup specifier. ('{color: red}')
184 #
185 #
185 attr_accessor :filter_html, :filter_styles
186 attr_accessor :filter_html, :filter_styles
186
187
187 #
188 #
188 # Accessor for toggling hard breaks.
189 # Accessor for toggling hard breaks.
189 #
190 #
190 # If +:hard_breaks+ is set, single newlines will
191 # If +:hard_breaks+ is set, single newlines will
191 # be converted to HTML break tags. This is the
192 # be converted to HTML break tags. This is the
192 # default behavior for traditional RedCloth.
193 # default behavior for traditional RedCloth.
193 #
194 #
194 attr_accessor :hard_breaks
195 attr_accessor :hard_breaks
195
196
196 # Accessor for toggling lite mode.
197 # Accessor for toggling lite mode.
197 #
198 #
198 # In lite mode, block-level rules are ignored. This means
199 # In lite mode, block-level rules are ignored. This means
199 # that tables, paragraphs, lists, and such aren't available.
200 # that tables, paragraphs, lists, and such aren't available.
200 # Only the inline markup for bold, italics, entities and so on.
201 # Only the inline markup for bold, italics, entities and so on.
201 #
202 #
202 # r = RedCloth.new( "And then? She *fell*!", [:lite_mode] )
203 # r = RedCloth.new( "And then? She *fell*!", [:lite_mode] )
203 # r.to_html
204 # r.to_html
204 # #=> "And then? She <strong>fell</strong>!"
205 # #=> "And then? She <strong>fell</strong>!"
205 #
206 #
206 attr_accessor :lite_mode
207 attr_accessor :lite_mode
207
208
208 #
209 #
209 # Accessor for toggling span caps.
210 # Accessor for toggling span caps.
210 #
211 #
211 # Textile places `span' tags around capitalized
212 # Textile places `span' tags around capitalized
212 # words by default, but this wreaks havoc on Wikis.
213 # words by default, but this wreaks havoc on Wikis.
213 # If +:no_span_caps+ is set, this will be
214 # If +:no_span_caps+ is set, this will be
214 # suppressed.
215 # suppressed.
215 #
216 #
216 attr_accessor :no_span_caps
217 attr_accessor :no_span_caps
217
218
218 #
219 #
219 # Establishes the markup predence. Available rules include:
220 # Establishes the markup predence. Available rules include:
220 #
221 #
221 # == Textile Rules
222 # == Textile Rules
222 #
223 #
223 # The following textile rules can be set individually. Or add the complete
224 # The following textile rules can be set individually. Or add the complete
224 # set of rules with the single :textile rule, which supplies the rule set in
225 # set of rules with the single :textile rule, which supplies the rule set in
225 # the following precedence:
226 # the following precedence:
226 #
227 #
227 # refs_textile:: Textile references (i.e. [hobix]http://hobix.com/)
228 # refs_textile:: Textile references (i.e. [hobix]http://hobix.com/)
228 # block_textile_table:: Textile table block structures
229 # block_textile_table:: Textile table block structures
229 # block_textile_lists:: Textile list structures
230 # block_textile_lists:: Textile list structures
230 # block_textile_prefix:: Textile blocks with prefixes (i.e. bq., h2., etc.)
231 # block_textile_prefix:: Textile blocks with prefixes (i.e. bq., h2., etc.)
231 # inline_textile_image:: Textile inline images
232 # inline_textile_image:: Textile inline images
232 # inline_textile_link:: Textile inline links
233 # inline_textile_link:: Textile inline links
233 # inline_textile_span:: Textile inline spans
234 # inline_textile_span:: Textile inline spans
234 # glyphs_textile:: Textile entities (such as em-dashes and smart quotes)
235 # glyphs_textile:: Textile entities (such as em-dashes and smart quotes)
235 #
236 #
236 # == Markdown
237 # == Markdown
237 #
238 #
238 # refs_markdown:: Markdown references (for example: [hobix]: http://hobix.com/)
239 # refs_markdown:: Markdown references (for example: [hobix]: http://hobix.com/)
239 # block_markdown_setext:: Markdown setext headers
240 # block_markdown_setext:: Markdown setext headers
240 # block_markdown_atx:: Markdown atx headers
241 # block_markdown_atx:: Markdown atx headers
241 # block_markdown_rule:: Markdown horizontal rules
242 # block_markdown_rule:: Markdown horizontal rules
242 # block_markdown_bq:: Markdown blockquotes
243 # block_markdown_bq:: Markdown blockquotes
243 # block_markdown_lists:: Markdown lists
244 # block_markdown_lists:: Markdown lists
244 # inline_markdown_link:: Markdown links
245 # inline_markdown_link:: Markdown links
245 attr_accessor :rules
246 attr_accessor :rules
246
247
247 # Returns a new RedCloth object, based on _string_ and
248 # Returns a new RedCloth object, based on _string_ and
248 # enforcing all the included _restrictions_.
249 # enforcing all the included _restrictions_.
249 #
250 #
250 # r = RedCloth.new( "h1. A <b>bold</b> man", [:filter_html] )
251 # r = RedCloth.new( "h1. A <b>bold</b> man", [:filter_html] )
251 # r.to_html
252 # r.to_html
252 # #=>"<h1>A &lt;b&gt;bold&lt;/b&gt; man</h1>"
253 # #=>"<h1>A &lt;b&gt;bold&lt;/b&gt; man</h1>"
253 #
254 #
254 def initialize( string, restrictions = [] )
255 def initialize( string, restrictions = [] )
255 restrictions.each { |r| method( "#{ r }=" ).call( true ) }
256 restrictions.each { |r| method( "#{ r }=" ).call( true ) }
256 super( string )
257 super( string )
257 end
258 end
258
259
259 #
260 #
260 # Generates HTML from the Textile contents.
261 # Generates HTML from the Textile contents.
261 #
262 #
262 # r = RedCloth.new( "And then? She *fell*!" )
263 # r = RedCloth.new( "And then? She *fell*!" )
263 # r.to_html( true )
264 # r.to_html( true )
264 # #=>"And then? She <strong>fell</strong>!"
265 # #=>"And then? She <strong>fell</strong>!"
265 #
266 #
266 def to_html( *rules )
267 def to_html( *rules )
267 rules = DEFAULT_RULES if rules.empty?
268 rules = DEFAULT_RULES if rules.empty?
268 # make our working copy
269 # make our working copy
269 text = self.dup
270 text = self.dup
270
271
271 @urlrefs = {}
272 @urlrefs = {}
272 @shelf = []
273 @shelf = []
273 textile_rules = [:block_textile_table, :block_textile_lists,
274 textile_rules = [:block_textile_table, :block_textile_lists,
274 :block_textile_prefix, :inline_textile_image, :inline_textile_link,
275 :block_textile_prefix, :inline_textile_image, :inline_textile_link,
275 :inline_textile_code, :inline_textile_span, :glyphs_textile]
276 :inline_textile_code, :inline_textile_span, :glyphs_textile]
276 markdown_rules = [:refs_markdown, :block_markdown_setext, :block_markdown_atx, :block_markdown_rule,
277 markdown_rules = [:refs_markdown, :block_markdown_setext, :block_markdown_atx, :block_markdown_rule,
277 :block_markdown_bq, :block_markdown_lists,
278 :block_markdown_bq, :block_markdown_lists,
278 :inline_markdown_reflink, :inline_markdown_link]
279 :inline_markdown_reflink, :inline_markdown_link]
279 @rules = rules.collect do |rule|
280 @rules = rules.collect do |rule|
280 case rule
281 case rule
281 when :markdown
282 when :markdown
282 markdown_rules
283 markdown_rules
283 when :textile
284 when :textile
284 textile_rules
285 textile_rules
285 else
286 else
286 rule
287 rule
287 end
288 end
288 end.flatten
289 end.flatten
289
290
290 # standard clean up
291 # standard clean up
291 incoming_entities text
292 incoming_entities text
292 clean_white_space text
293 clean_white_space text
293
294
294 # start processor
295 # start processor
295 @pre_list = []
296 @pre_list = []
296 rip_offtags text
297 rip_offtags text
297 no_textile text
298 no_textile text
298 escape_html_tags text
299 escape_html_tags text
299 # need to do this before #hard_break and #blocks
300 # need to do this before #hard_break and #blocks
300 block_textile_quotes text unless @lite_mode
301 block_textile_quotes text unless @lite_mode
301 hard_break text
302 hard_break text
302 unless @lite_mode
303 unless @lite_mode
303 refs text
304 refs text
304 blocks text
305 blocks text
305 end
306 end
306 inline text
307 inline text
307 smooth_offtags text
308 smooth_offtags text
308
309
309 retrieve text
310 retrieve text
310
311
311 text.gsub!( /<\/?notextile>/, '' )
312 text.gsub!( /<\/?notextile>/, '' )
312 text.gsub!( /x%x%/, '&#38;' )
313 text.gsub!( /x%x%/, '&#38;' )
313 clean_html text if filter_html
314 clean_html text if filter_html
314 text.strip!
315 text.strip!
315 text
316 text
316
317
317 end
318 end
318
319
319 #######
320 #######
320 private
321 private
321 #######
322 #######
322 #
323 #
323 # Mapping of 8-bit ASCII codes to HTML numerical entity equivalents.
324 # Mapping of 8-bit ASCII codes to HTML numerical entity equivalents.
324 # (from PyTextile)
325 # (from PyTextile)
325 #
326 #
326 TEXTILE_TAGS =
327 TEXTILE_TAGS =
327
328
328 [[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230],
329 [[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230],
329 [134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249],
330 [134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249],
330 [140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217],
331 [140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217],
331 [147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732],
332 [147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732],
332 [153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]].
333 [153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]].
333
334
334 collect! do |a, b|
335 collect! do |a, b|
335 [a.chr, ( b.zero? and "" or "&#{ b };" )]
336 [a.chr, ( b.zero? and "" or "&#{ b };" )]
336 end
337 end
337
338
338 #
339 #
339 # Regular expressions to convert to HTML.
340 # Regular expressions to convert to HTML.
340 #
341 #
341 A_HLGN = /(?:(?:<>|<|>|\=|[()]+)+)/
342 A_HLGN = /(?:(?:<>|<|>|\=|[()]+)+)/
342 A_VLGN = /[\-^~]/
343 A_VLGN = /[\-^~]/
343 C_CLAS = '(?:\([^")]+\))'
344 C_CLAS = '(?:\([^")]+\))'
344 C_LNGE = '(?:\[[a-z\-_]+\])'
345 C_LNGE = '(?:\[[a-z\-_]+\])'
345 C_STYL = '(?:\{[^"}]+\})'
346 C_STYL = '(?:\{[^"}]+\})'
346 S_CSPN = '(?:\\\\\d+)'
347 S_CSPN = '(?:\\\\\d+)'
347 S_RSPN = '(?:/\d+)'
348 S_RSPN = '(?:/\d+)'
348 A = "(?:#{A_HLGN}?#{A_VLGN}?|#{A_VLGN}?#{A_HLGN}?)"
349 A = "(?:#{A_HLGN}?#{A_VLGN}?|#{A_VLGN}?#{A_HLGN}?)"
349 S = "(?:#{S_CSPN}?#{S_RSPN}|#{S_RSPN}?#{S_CSPN}?)"
350 S = "(?:#{S_CSPN}?#{S_RSPN}|#{S_RSPN}?#{S_CSPN}?)"
350 C = "(?:#{C_CLAS}?#{C_STYL}?#{C_LNGE}?|#{C_STYL}?#{C_LNGE}?#{C_CLAS}?|#{C_LNGE}?#{C_STYL}?#{C_CLAS}?)"
351 C = "(?:#{C_CLAS}?#{C_STYL}?#{C_LNGE}?|#{C_STYL}?#{C_LNGE}?#{C_CLAS}?|#{C_LNGE}?#{C_STYL}?#{C_CLAS}?)"
351 # PUNCT = Regexp::quote( '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' )
352 # PUNCT = Regexp::quote( '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' )
352 PUNCT = Regexp::quote( '!"#$%&\'*+,-./:;=?@\\^_`|~' )
353 PUNCT = Regexp::quote( '!"#$%&\'*+,-./:;=?@\\^_`|~' )
353 PUNCT_NOQ = Regexp::quote( '!"#$&\',./:;=?@\\`|' )
354 PUNCT_NOQ = Regexp::quote( '!"#$&\',./:;=?@\\`|' )
354 PUNCT_Q = Regexp::quote( '*-_+^~%' )
355 PUNCT_Q = Regexp::quote( '*-_+^~%' )
355 HYPERLINK = '(\S+?)([^\w\s/;=\?]*?)(?=\s|<|$)'
356 HYPERLINK = '(\S+?)([^\w\s/;=\?]*?)(?=\s|<|$)'
356
357
357 # Text markup tags, don't conflict with block tags
358 # Text markup tags, don't conflict with block tags
358 SIMPLE_HTML_TAGS = [
359 SIMPLE_HTML_TAGS = [
359 'tt', 'b', 'i', 'big', 'small', 'em', 'strong', 'dfn', 'code',
360 'tt', 'b', 'i', 'big', 'small', 'em', 'strong', 'dfn', 'code',
360 'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'a', 'img', 'br',
361 'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'a', 'img', 'br',
361 'br', 'map', 'q', 'sub', 'sup', 'span', 'bdo'
362 'br', 'map', 'q', 'sub', 'sup', 'span', 'bdo'
362 ]
363 ]
363
364
364 QTAGS = [
365 QTAGS = [
365 ['**', 'b', :limit],
366 ['**', 'b', :limit],
366 ['*', 'strong', :limit],
367 ['*', 'strong', :limit],
367 ['??', 'cite', :limit],
368 ['??', 'cite', :limit],
368 ['-', 'del', :limit],
369 ['-', 'del', :limit],
369 ['__', 'i', :limit],
370 ['__', 'i', :limit],
370 ['_', 'em', :limit],
371 ['_', 'em', :limit],
371 ['%', 'span', :limit],
372 ['%', 'span', :limit],
372 ['+', 'ins', :limit],
373 ['+', 'ins', :limit],
373 ['^', 'sup', :limit],
374 ['^', 'sup', :limit],
374 ['~', 'sub', :limit]
375 ['~', 'sub', :limit]
375 ]
376 ]
376 QTAGS_JOIN = QTAGS.map {|rc, ht, rtype| Regexp::quote rc}.join('|')
377 QTAGS_JOIN = QTAGS.map {|rc, ht, rtype| Regexp::quote rc}.join('|')
377
378
378 QTAGS.collect! do |rc, ht, rtype|
379 QTAGS.collect! do |rc, ht, rtype|
379 rcq = Regexp::quote rc
380 rcq = Regexp::quote rc
380 re =
381 re =
381 case rtype
382 case rtype
382 when :limit
383 when :limit
383 /(^|[>\s\(]) # sta
384 /(^|[>\s\(]) # sta
384 (?!\-\-)
385 (?!\-\-)
385 (#{QTAGS_JOIN}|) # oqs
386 (#{QTAGS_JOIN}|) # oqs
386 (#{rcq}) # qtag
387 (#{rcq}) # qtag
387 ([[:word:]]|[^\s].*?[^\s]) # content
388 ([[:word:]]|[^\s].*?[^\s]) # content
388 (?!\-\-)
389 (?!\-\-)
389 #{rcq}
390 #{rcq}
390 (#{QTAGS_JOIN}|) # oqa
391 (#{QTAGS_JOIN}|) # oqa
391 (?=[[:punct:]]|<|\s|\)|$)/x
392 (?=[[:punct:]]|<|\s|\)|$)/x
392 else
393 else
393 /(#{rcq})
394 /(#{rcq})
394 (#{C})
395 (#{C})
395 (?::(\S+))?
396 (?::(\S+))?
396 ([[:word:]]|[^\s\-].*?[^\s\-])
397 ([[:word:]]|[^\s\-].*?[^\s\-])
397 #{rcq}/xm
398 #{rcq}/xm
398 end
399 end
399 [rc, ht, re, rtype]
400 [rc, ht, re, rtype]
400 end
401 end
401
402
402 # Elements to handle
403 # Elements to handle
403 GLYPHS = [
404 GLYPHS = [
404 # [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1&#8217;\2' ], # single closing
405 # [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1&#8217;\2' ], # single closing
405 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)\'/, '\1&#8217;' ], # single closing
406 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)\'/, '\1&#8217;' ], # single closing
406 # [ /\'(?=[#{PUNCT_Q}]*(s\b|[\s#{PUNCT_NOQ}]))/, '&#8217;' ], # single closing
407 # [ /\'(?=[#{PUNCT_Q}]*(s\b|[\s#{PUNCT_NOQ}]))/, '&#8217;' ], # single closing
407 # [ /\'/, '&#8216;' ], # single opening
408 # [ /\'/, '&#8216;' ], # single opening
408 # [ /</, '&lt;' ], # less-than
409 # [ /</, '&lt;' ], # less-than
409 # [ />/, '&gt;' ], # greater-than
410 # [ />/, '&gt;' ], # greater-than
410 # [ /([^\s\[{(])?"(\s|:|$)/, '\1&#8221;\2' ], # double closing
411 # [ /([^\s\[{(])?"(\s|:|$)/, '\1&#8221;\2' ], # double closing
411 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)"/, '\1&#8221;' ], # double closing
412 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)"/, '\1&#8221;' ], # double closing
412 # [ /"(?=[#{PUNCT_Q}]*[\s#{PUNCT_NOQ}])/, '&#8221;' ], # double closing
413 # [ /"(?=[#{PUNCT_Q}]*[\s#{PUNCT_NOQ}])/, '&#8221;' ], # double closing
413 # [ /"/, '&#8220;' ], # double opening
414 # [ /"/, '&#8220;' ], # double opening
414 # [ /\b( )?\.{3}/, '\1&#8230;' ], # ellipsis
415 # [ /\b( )?\.{3}/, '\1&#8230;' ], # ellipsis
415 # [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '<acronym title="\2">\1</acronym>' ], # 3+ uppercase acronym
416 # [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '<acronym title="\2">\1</acronym>' ], # 3+ uppercase acronym
416 # [ /(^|[^"][>\s])([A-Z][A-Z0-9 ]+[A-Z0-9])([^<A-Za-z0-9]|$)/, '\1<span class="caps">\2</span>\3', :no_span_caps ], # 3+ uppercase caps
417 # [ /(^|[^"][>\s])([A-Z][A-Z0-9 ]+[A-Z0-9])([^<A-Za-z0-9]|$)/, '\1<span class="caps">\2</span>\3', :no_span_caps ], # 3+ uppercase caps
417 # [ /(\.\s)?\s?--\s?/, '\1&#8212;' ], # em dash
418 # [ /(\.\s)?\s?--\s?/, '\1&#8212;' ], # em dash
418 # [ /\s->\s/, ' &rarr; ' ], # right arrow
419 # [ /\s->\s/, ' &rarr; ' ], # right arrow
419 # [ /\s-\s/, ' &#8211; ' ], # en dash
420 # [ /\s-\s/, ' &#8211; ' ], # en dash
420 # [ /(\d+) ?x ?(\d+)/, '\1&#215;\2' ], # dimension sign
421 # [ /(\d+) ?x ?(\d+)/, '\1&#215;\2' ], # dimension sign
421 # [ /\b ?[(\[]TM[\])]/i, '&#8482;' ], # trademark
422 # [ /\b ?[(\[]TM[\])]/i, '&#8482;' ], # trademark
422 # [ /\b ?[(\[]R[\])]/i, '&#174;' ], # registered
423 # [ /\b ?[(\[]R[\])]/i, '&#174;' ], # registered
423 # [ /\b ?[(\[]C[\])]/i, '&#169;' ] # copyright
424 # [ /\b ?[(\[]C[\])]/i, '&#169;' ] # copyright
424 ]
425 ]
425
426
426 H_ALGN_VALS = {
427 H_ALGN_VALS = {
427 '<' => 'left',
428 '<' => 'left',
428 '=' => 'center',
429 '=' => 'center',
429 '>' => 'right',
430 '>' => 'right',
430 '<>' => 'justify'
431 '<>' => 'justify'
431 }
432 }
432
433
433 V_ALGN_VALS = {
434 V_ALGN_VALS = {
434 '^' => 'top',
435 '^' => 'top',
435 '-' => 'middle',
436 '-' => 'middle',
436 '~' => 'bottom'
437 '~' => 'bottom'
437 }
438 }
438
439
439 #
440 #
440 # Flexible HTML escaping
441 # Flexible HTML escaping
441 #
442 #
442 def htmlesc( str, mode=:Quotes )
443 def htmlesc( str, mode=:Quotes )
443 if str
444 if str
444 str.gsub!( '&', '&amp;' )
445 str.gsub!( '&', '&amp;' )
445 str.gsub!( '"', '&quot;' ) if mode != :NoQuotes
446 str.gsub!( '"', '&quot;' ) if mode != :NoQuotes
446 str.gsub!( "'", '&#039;' ) if mode == :Quotes
447 str.gsub!( "'", '&#039;' ) if mode == :Quotes
447 str.gsub!( '<', '&lt;')
448 str.gsub!( '<', '&lt;')
448 str.gsub!( '>', '&gt;')
449 str.gsub!( '>', '&gt;')
449 end
450 end
450 str
451 str
451 end
452 end
452
453
453 # Search and replace for Textile glyphs (quotes, dashes, other symbols)
454 # Search and replace for Textile glyphs (quotes, dashes, other symbols)
454 def pgl( text )
455 def pgl( text )
455 #GLYPHS.each do |re, resub, tog|
456 #GLYPHS.each do |re, resub, tog|
456 # next if tog and method( tog ).call
457 # next if tog and method( tog ).call
457 # text.gsub! re, resub
458 # text.gsub! re, resub
458 #end
459 #end
459 text.gsub!(/\b([A-Z][A-Z0-9]{1,})\b(?:[(]([^)]*)[)])/) do |m|
460 text.gsub!(/\b([A-Z][A-Z0-9]{1,})\b(?:[(]([^)]*)[)])/) do |m|
460 "<abbr title=\"#{htmlesc $2}\">#{$1}</abbr>"
461 "<abbr title=\"#{htmlesc $2}\">#{$1}</abbr>"
461 end
462 end
462 end
463 end
463
464
464 # Parses Textile attribute lists and builds an HTML attribute string
465 # Parses Textile attribute lists and builds an HTML attribute string
465 def pba( text_in, element = "" )
466 def pba( text_in, element = "" )
466
467
467 return '' unless text_in
468 return '' unless text_in
468
469
469 style = []
470 style = []
470 text = text_in.dup
471 text = text_in.dup
471 if element == 'td'
472 if element == 'td'
472 colspan = $1 if text =~ /\\(\d+)/
473 colspan = $1 if text =~ /\\(\d+)/
473 rowspan = $1 if text =~ /\/(\d+)/
474 rowspan = $1 if text =~ /\/(\d+)/
474 style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN
475 style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN
475 end
476 end
476
477
477 if text.sub!( /\{([^"}]*)\}/, '' ) && !filter_styles
478 if text.sub!( /\{([^"}]*)\}/, '' ) && !filter_styles
478 sanitized = sanitize_styles($1)
479 sanitized = sanitize_styles($1)
479 style << "#{ sanitized };" unless sanitized.blank?
480 style << "#{ sanitized };" unless sanitized.blank?
480 end
481 end
481
482
482 lang = $1 if
483 lang = $1 if
483 text.sub!( /\[([a-z\-_]+?)\]/, '' )
484 text.sub!( /\[([a-z\-_]+?)\]/, '' )
484
485
485 cls = $1 if
486 cls = $1 if
486 text.sub!( /\(([^()]+?)\)/, '' )
487 text.sub!( /\(([^()]+?)\)/, '' )
487
488
488 style << "padding-left:#{ $1.length }em;" if
489 style << "padding-left:#{ $1.length }em;" if
489 text.sub!( /([(]+)/, '' )
490 text.sub!( /([(]+)/, '' )
490
491
491 style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' )
492 style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' )
492
493
493 style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN
494 style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN
494
495
495 cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/
496 cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/
496
497
497 atts = ''
498 atts = ''
498 atts << " style=\"#{ style.join }\"" unless style.empty?
499 atts << " style=\"#{ style.join }\"" unless style.empty?
499 atts << " class=\"#{ cls }\"" unless cls.to_s.empty?
500 atts << " class=\"#{ cls }\"" unless cls.to_s.empty?
500 atts << " lang=\"#{ lang }\"" if lang
501 atts << " lang=\"#{ lang }\"" if lang
501 atts << " id=\"#{ id }\"" if id
502 atts << " id=\"#{ id }\"" if id
502 atts << " colspan=\"#{ colspan }\"" if colspan
503 atts << " colspan=\"#{ colspan }\"" if colspan
503 atts << " rowspan=\"#{ rowspan }\"" if rowspan
504 atts << " rowspan=\"#{ rowspan }\"" if rowspan
504
505
505 atts
506 atts
506 end
507 end
507
508
508 STYLES_RE = /^(color|width|height|border|background|padding|margin|font|text|float)(-[a-z]+)*:\s*((\d+%?|\d+px|\d+(\.\d+)?em|#[0-9a-f]+|[a-z]+)\s*)+$/i
509 STYLES_RE = /^(color|width|height|border|background|padding|margin|font|text|float)(-[a-z]+)*:\s*((\d+%?|\d+px|\d+(\.\d+)?em|#[0-9a-f]+|[a-z]+)\s*)+$/i
509
510
510 def sanitize_styles(str)
511 def sanitize_styles(str)
511 styles = str.split(";").map(&:strip)
512 styles = str.split(";").map(&:strip)
512 styles.reject! do |style|
513 styles.reject! do |style|
513 !style.match(STYLES_RE)
514 !style.match(STYLES_RE)
514 end
515 end
515 styles.join(";")
516 styles.join(";")
516 end
517 end
517
518
518 TABLE_RE = /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)(\n\n|\Z)/m
519 TABLE_RE = /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)(\n\n|\Z)/m
519
520
520 # Parses a Textile table block, building HTML from the result.
521 # Parses a Textile table block, building HTML from the result.
521 def block_textile_table( text )
522 def block_textile_table( text )
522 text.gsub!( TABLE_RE ) do |matches|
523 text.gsub!( TABLE_RE ) do |matches|
523
524
524 tatts, fullrow = $~[1..2]
525 tatts, fullrow = $~[1..2]
525 tatts = pba( tatts, 'table' )
526 tatts = pba( tatts, 'table' )
526 tatts = shelve( tatts ) if tatts
527 tatts = shelve( tatts ) if tatts
527 rows = []
528 rows = []
528 fullrow.gsub!(/([^|\s])\s*\n/, "\\1<br />")
529 fullrow.gsub!(/([^|\s])\s*\n/, "\\1<br />")
529 fullrow.each_line do |row|
530 fullrow.each_line do |row|
530 ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m
531 ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m
531 cells = []
532 cells = []
532 # the regexp prevents wiki links with a | from being cut as cells
533 # the regexp prevents wiki links with a | from being cut as cells
533 row.scan(/\|(_?#{S}#{A}#{C}\. ?)?((\[\[[^|\]]*\|[^|\]]*\]\]|[^|])*?)(?=\|)/) do |modifiers, cell|
534 row.scan(/\|(_?#{S}#{A}#{C}\. ?)?((\[\[[^|\]]*\|[^|\]]*\]\]|[^|])*?)(?=\|)/) do |modifiers, cell|
534 ctyp = 'd'
535 ctyp = 'd'
535 ctyp = 'h' if modifiers && modifiers =~ /^_/
536 ctyp = 'h' if modifiers && modifiers =~ /^_/
536
537
537 catts = nil
538 catts = nil
538 catts = pba( modifiers, 'td' ) if modifiers
539 catts = pba( modifiers, 'td' ) if modifiers
539
540
540 catts = shelve( catts ) if catts
541 catts = shelve( catts ) if catts
541 cells << "\t\t\t<t#{ ctyp }#{ catts }>#{ cell }</t#{ ctyp }>"
542 cells << "\t\t\t<t#{ ctyp }#{ catts }>#{ cell }</t#{ ctyp }>"
542 end
543 end
543 ratts = shelve( ratts ) if ratts
544 ratts = shelve( ratts ) if ratts
544 rows << "\t\t<tr#{ ratts }>\n#{ cells.join( "\n" ) }\n\t\t</tr>"
545 rows << "\t\t<tr#{ ratts }>\n#{ cells.join( "\n" ) }\n\t\t</tr>"
545 end
546 end
546 "\t<table#{ tatts }>\n#{ rows.join( "\n" ) }\n\t</table>\n\n"
547 "\t<table#{ tatts }>\n#{ rows.join( "\n" ) }\n\t</table>\n\n"
547 end
548 end
548 end
549 end
549
550
550 LISTS_RE = /^([#*]+?#{C} .*?)$(?![^#*])/m
551 LISTS_RE = /^([#*]+?#{C} .*?)$(?![^#*])/m
551 LISTS_CONTENT_RE = /^([#*]+)(#{A}#{C}) (.*)$/m
552 LISTS_CONTENT_RE = /^([#*]+)(#{A}#{C}) (.*)$/m
552
553
553 # Parses Textile lists and generates HTML
554 # Parses Textile lists and generates HTML
554 def block_textile_lists( text )
555 def block_textile_lists( text )
555 text.gsub!( LISTS_RE ) do |match|
556 text.gsub!( LISTS_RE ) do |match|
556 lines = match.split( /\n/ )
557 lines = match.split( /\n/ )
557 last_line = -1
558 last_line = -1
558 depth = []
559 depth = []
559 lines.each_with_index do |line, line_id|
560 lines.each_with_index do |line, line_id|
560 if line =~ LISTS_CONTENT_RE
561 if line =~ LISTS_CONTENT_RE
561 tl,atts,content = $~[1..3]
562 tl,atts,content = $~[1..3]
562 if depth.last
563 if depth.last
563 if depth.last.length > tl.length
564 if depth.last.length > tl.length
564 (depth.length - 1).downto(0) do |i|
565 (depth.length - 1).downto(0) do |i|
565 break if depth[i].length == tl.length
566 break if depth[i].length == tl.length
566 lines[line_id - 1] << "</li>\n\t</#{ lT( depth[i] ) }l>\n\t"
567 lines[line_id - 1] << "</li>\n\t</#{ lT( depth[i] ) }l>\n\t"
567 depth.pop
568 depth.pop
568 end
569 end
569 end
570 end
570 if depth.last and depth.last.length == tl.length
571 if depth.last and depth.last.length == tl.length
571 lines[line_id - 1] << '</li>'
572 lines[line_id - 1] << '</li>'
572 end
573 end
573 end
574 end
574 unless depth.last == tl
575 unless depth.last == tl
575 depth << tl
576 depth << tl
576 atts = pba( atts )
577 atts = pba( atts )
577 atts = shelve( atts ) if atts
578 atts = shelve( atts ) if atts
578 lines[line_id] = "\t<#{ lT(tl) }l#{ atts }>\n\t<li>#{ content }"
579 lines[line_id] = "\t<#{ lT(tl) }l#{ atts }>\n\t<li>#{ content }"
579 else
580 else
580 lines[line_id] = "\t\t<li>#{ content }"
581 lines[line_id] = "\t\t<li>#{ content }"
581 end
582 end
582 last_line = line_id
583 last_line = line_id
583
584
584 else
585 else
585 last_line = line_id
586 last_line = line_id
586 end
587 end
587 if line_id - last_line > 1 or line_id == lines.length - 1
588 if line_id - last_line > 1 or line_id == lines.length - 1
588 while v = depth.pop
589 while v = depth.pop
589 lines[last_line] << "</li>\n\t</#{ lT( v ) }l>"
590 lines[last_line] << "</li>\n\t</#{ lT( v ) }l>"
590 end
591 end
591 end
592 end
592 end
593 end
593 lines.join( "\n" )
594 lines.join( "\n" )
594 end
595 end
595 end
596 end
596
597
597 QUOTES_RE = /(^>+([^\n]*?)(\n|$))+/m
598 QUOTES_RE = /(^>+([^\n]*?)(\n|$))+/m
598 QUOTES_CONTENT_RE = /^([> ]+)(.*)$/m
599 QUOTES_CONTENT_RE = /^([> ]+)(.*)$/m
599
600
600 def block_textile_quotes( text )
601 def block_textile_quotes( text )
601 text.gsub!( QUOTES_RE ) do |match|
602 text.gsub!( QUOTES_RE ) do |match|
602 lines = match.split( /\n/ )
603 lines = match.split( /\n/ )
603 quotes = ''
604 quotes = ''
604 indent = 0
605 indent = 0
605 lines.each do |line|
606 lines.each do |line|
606 line =~ QUOTES_CONTENT_RE
607 line =~ QUOTES_CONTENT_RE
607 bq,content = $1, $2
608 bq,content = $1, $2
608 l = bq.count('>')
609 l = bq.count('>')
609 if l != indent
610 if l != indent
610 quotes << ("\n\n" + (l>indent ? '<blockquote>' * (l-indent) : '</blockquote>' * (indent-l)) + "\n\n")
611 quotes << ("\n\n" + (l>indent ? '<blockquote>' * (l-indent) : '</blockquote>' * (indent-l)) + "\n\n")
611 indent = l
612 indent = l
612 end
613 end
613 quotes << (content + "\n")
614 quotes << (content + "\n")
614 end
615 end
615 quotes << ("\n" + '</blockquote>' * indent + "\n\n")
616 quotes << ("\n" + '</blockquote>' * indent + "\n\n")
616 quotes
617 quotes
617 end
618 end
618 end
619 end
619
620
620 CODE_RE = /(\W)
621 CODE_RE = /(\W)
621 @
622 @
622 (?:\|(\w+?)\|)?
623 (?:\|(\w+?)\|)?
623 (.+?)
624 (.+?)
624 @
625 @
625 (?=\W)/x
626 (?=\W)/x
626
627
627 def inline_textile_code( text )
628 def inline_textile_code( text )
628 text.gsub!( CODE_RE ) do |m|
629 text.gsub!( CODE_RE ) do |m|
629 before,lang,code,after = $~[1..4]
630 before,lang,code,after = $~[1..4]
630 lang = " lang=\"#{ lang }\"" if lang
631 lang = " lang=\"#{ lang }\"" if lang
631 rip_offtags( "#{ before }<code#{ lang }>#{ code }</code>#{ after }", false )
632 rip_offtags( "#{ before }<code#{ lang }>#{ code }</code>#{ after }", false )
632 end
633 end
633 end
634 end
634
635
635 def lT( text )
636 def lT( text )
636 text =~ /\#$/ ? 'o' : 'u'
637 text =~ /\#$/ ? 'o' : 'u'
637 end
638 end
638
639
639 def hard_break( text )
640 def hard_break( text )
640 text.gsub!( /(.)\n(?!\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
641 text.gsub!( /(.)\n(?!\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
641 end
642 end
642
643
643 BLOCKS_GROUP_RE = /\n{2,}(?! )/m
644 BLOCKS_GROUP_RE = /\n{2,}(?! )/m
644
645
645 def blocks( text, deep_code = false )
646 def blocks( text, deep_code = false )
646 text.replace( text.split( BLOCKS_GROUP_RE ).collect do |blk|
647 text.replace( text.split( BLOCKS_GROUP_RE ).collect do |blk|
647 plain = blk !~ /\A[#*> ]/
648 plain = blk !~ /\A[#*> ]/
648
649
649 # skip blocks that are complex HTML
650 # skip blocks that are complex HTML
650 if blk =~ /^<\/?(\w+).*>/ and not SIMPLE_HTML_TAGS.include? $1
651 if blk =~ /^<\/?(\w+).*>/ and not SIMPLE_HTML_TAGS.include? $1
651 blk
652 blk
652 else
653 else
653 # search for indentation levels
654 # search for indentation levels
654 blk.strip!
655 blk.strip!
655 if blk.empty?
656 if blk.empty?
656 blk
657 blk
657 else
658 else
658 code_blk = nil
659 code_blk = nil
659 blk.gsub!( /((?:\n(?:\n^ +[^\n]*)+)+)/m ) do |iblk|
660 blk.gsub!( /((?:\n(?:\n^ +[^\n]*)+)+)/m ) do |iblk|
660 flush_left iblk
661 flush_left iblk
661 blocks iblk, plain
662 blocks iblk, plain
662 iblk.gsub( /^(\S)/, "\t\\1" )
663 iblk.gsub( /^(\S)/, "\t\\1" )
663 if plain
664 if plain
664 code_blk = iblk; ""
665 code_blk = iblk; ""
665 else
666 else
666 iblk
667 iblk
667 end
668 end
668 end
669 end
669
670
670 block_applied = 0
671 block_applied = 0
671 @rules.each do |rule_name|
672 @rules.each do |rule_name|
672 block_applied += 1 if ( rule_name.to_s.match /^block_/ and method( rule_name ).call( blk ) )
673 block_applied += 1 if ( rule_name.to_s.match /^block_/ and method( rule_name ).call( blk ) )
673 end
674 end
674 if block_applied.zero?
675 if block_applied.zero?
675 if deep_code
676 if deep_code
676 blk = "\t<pre><code>#{ blk }</code></pre>"
677 blk = "\t<pre><code>#{ blk }</code></pre>"
677 else
678 else
678 blk = "\t<p>#{ blk }</p>"
679 blk = "\t<p>#{ blk }</p>"
679 end
680 end
680 end
681 end
681 # hard_break blk
682 # hard_break blk
682 blk + "\n#{ code_blk }"
683 blk + "\n#{ code_blk }"
683 end
684 end
684 end
685 end
685
686
686 end.join( "\n\n" ) )
687 end.join( "\n\n" ) )
687 end
688 end
688
689
689 def textile_bq( tag, atts, cite, content )
690 def textile_bq( tag, atts, cite, content )
690 cite, cite_title = check_refs( cite )
691 cite, cite_title = check_refs( cite )
691 cite = " cite=\"#{ cite }\"" if cite
692 cite = " cite=\"#{ cite }\"" if cite
692 atts = shelve( atts ) if atts
693 atts = shelve( atts ) if atts
693 "\t<blockquote#{ cite }>\n\t\t<p#{ atts }>#{ content }</p>\n\t</blockquote>"
694 "\t<blockquote#{ cite }>\n\t\t<p#{ atts }>#{ content }</p>\n\t</blockquote>"
694 end
695 end
695
696
696 def textile_p( tag, atts, cite, content )
697 def textile_p( tag, atts, cite, content )
697 atts = shelve( atts ) if atts
698 atts = shelve( atts ) if atts
698 "\t<#{ tag }#{ atts }>#{ content }</#{ tag }>"
699 "\t<#{ tag }#{ atts }>#{ content }</#{ tag }>"
699 end
700 end
700
701
701 alias textile_h1 textile_p
702 alias textile_h1 textile_p
702 alias textile_h2 textile_p
703 alias textile_h2 textile_p
703 alias textile_h3 textile_p
704 alias textile_h3 textile_p
704 alias textile_h4 textile_p
705 alias textile_h4 textile_p
705 alias textile_h5 textile_p
706 alias textile_h5 textile_p
706 alias textile_h6 textile_p
707 alias textile_h6 textile_p
707
708
708 def textile_fn_( tag, num, atts, cite, content )
709 def textile_fn_( tag, num, atts, cite, content )
709 atts << " id=\"fn#{ num }\" class=\"footnote\""
710 atts << " id=\"fn#{ num }\" class=\"footnote\""
710 content = "<sup>#{ num }</sup> #{ content }"
711 content = "<sup>#{ num }</sup> #{ content }"
711 atts = shelve( atts ) if atts
712 atts = shelve( atts ) if atts
712 "\t<p#{ atts }>#{ content }</p>"
713 "\t<p#{ atts }>#{ content }</p>"
713 end
714 end
714
715
715 BLOCK_RE = /^(([a-z]+)(\d*))(#{A}#{C})\.(?::(\S+))? (.*)$/m
716 BLOCK_RE = /^(([a-z]+)(\d*))(#{A}#{C})\.(?::(\S+))? (.*)$/m
716
717
717 def block_textile_prefix( text )
718 def block_textile_prefix( text )
718 if text =~ BLOCK_RE
719 if text =~ BLOCK_RE
719 tag,tagpre,num,atts,cite,content = $~[1..6]
720 tag,tagpre,num,atts,cite,content = $~[1..6]
720 atts = pba( atts )
721 atts = pba( atts )
721
722
722 # pass to prefix handler
723 # pass to prefix handler
723 replacement = nil
724 replacement = nil
724 if respond_to? "textile_#{ tag }", true
725 if respond_to? "textile_#{ tag }", true
725 replacement = method( "textile_#{ tag }" ).call( tag, atts, cite, content )
726 replacement = method( "textile_#{ tag }" ).call( tag, atts, cite, content )
726 elsif respond_to? "textile_#{ tagpre }_", true
727 elsif respond_to? "textile_#{ tagpre }_", true
727 replacement = method( "textile_#{ tagpre }_" ).call( tagpre, num, atts, cite, content )
728 replacement = method( "textile_#{ tagpre }_" ).call( tagpre, num, atts, cite, content )
728 end
729 end
729 text.gsub!( $& ) { replacement } if replacement
730 text.gsub!( $& ) { replacement } if replacement
730 end
731 end
731 end
732 end
732
733
733 SETEXT_RE = /\A(.+?)\n([=-])[=-]* *$/m
734 SETEXT_RE = /\A(.+?)\n([=-])[=-]* *$/m
734 def block_markdown_setext( text )
735 def block_markdown_setext( text )
735 if text =~ SETEXT_RE
736 if text =~ SETEXT_RE
736 tag = if $2 == "="; "h1"; else; "h2"; end
737 tag = if $2 == "="; "h1"; else; "h2"; end
737 blk, cont = "<#{ tag }>#{ $1 }</#{ tag }>", $'
738 blk, cont = "<#{ tag }>#{ $1 }</#{ tag }>", $'
738 blocks cont
739 blocks cont
739 text.replace( blk + cont )
740 text.replace( blk + cont )
740 end
741 end
741 end
742 end
742
743
743 ATX_RE = /\A(\#{1,6}) # $1 = string of #'s
744 ATX_RE = /\A(\#{1,6}) # $1 = string of #'s
744 [ ]*
745 [ ]*
745 (.+?) # $2 = Header text
746 (.+?) # $2 = Header text
746 [ ]*
747 [ ]*
747 \#* # optional closing #'s (not counted)
748 \#* # optional closing #'s (not counted)
748 $/x
749 $/x
749 def block_markdown_atx( text )
750 def block_markdown_atx( text )
750 if text =~ ATX_RE
751 if text =~ ATX_RE
751 tag = "h#{ $1.length }"
752 tag = "h#{ $1.length }"
752 blk, cont = "<#{ tag }>#{ $2 }</#{ tag }>\n\n", $'
753 blk, cont = "<#{ tag }>#{ $2 }</#{ tag }>\n\n", $'
753 blocks cont
754 blocks cont
754 text.replace( blk + cont )
755 text.replace( blk + cont )
755 end
756 end
756 end
757 end
757
758
758 MARKDOWN_BQ_RE = /\A(^ *> ?.+$(.+\n)*\n*)+/m
759 MARKDOWN_BQ_RE = /\A(^ *> ?.+$(.+\n)*\n*)+/m
759
760
760 def block_markdown_bq( text )
761 def block_markdown_bq( text )
761 text.gsub!( MARKDOWN_BQ_RE ) do |blk|
762 text.gsub!( MARKDOWN_BQ_RE ) do |blk|
762 blk.gsub!( /^ *> ?/, '' )
763 blk.gsub!( /^ *> ?/, '' )
763 flush_left blk
764 flush_left blk
764 blocks blk
765 blocks blk
765 blk.gsub!( /^(\S)/, "\t\\1" )
766 blk.gsub!( /^(\S)/, "\t\\1" )
766 "<blockquote>\n#{ blk }\n</blockquote>\n\n"
767 "<blockquote>\n#{ blk }\n</blockquote>\n\n"
767 end
768 end
768 end
769 end
769
770
770 MARKDOWN_RULE_RE = /^(#{
771 MARKDOWN_RULE_RE = /^(#{
771 ['*', '-', '_'].collect { |ch| ' ?(' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' )
772 ['*', '-', '_'].collect { |ch| ' ?(' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' )
772 })$/
773 })$/
773
774
774 def block_markdown_rule( text )
775 def block_markdown_rule( text )
775 text.gsub!( MARKDOWN_RULE_RE ) do |blk|
776 text.gsub!( MARKDOWN_RULE_RE ) do |blk|
776 "<hr />"
777 "<hr />"
777 end
778 end
778 end
779 end
779
780
780 # XXX TODO XXX
781 # XXX TODO XXX
781 def block_markdown_lists( text )
782 def block_markdown_lists( text )
782 end
783 end
783
784
784 def inline_textile_span( text )
785 def inline_textile_span( text )
785 QTAGS.each do |qtag_rc, ht, qtag_re, rtype|
786 QTAGS.each do |qtag_rc, ht, qtag_re, rtype|
786 text.gsub!( qtag_re ) do |m|
787 text.gsub!( qtag_re ) do |m|
787
788
788 case rtype
789 case rtype
789 when :limit
790 when :limit
790 sta,oqs,qtag,content,oqa = $~[1..6]
791 sta,oqs,qtag,content,oqa = $~[1..6]
791 atts = nil
792 atts = nil
792 if content =~ /^(#{C})(.+)$/
793 if content =~ /^(#{C})(.+)$/
793 atts, content = $~[1..2]
794 atts, content = $~[1..2]
794 end
795 end
795 else
796 else
796 qtag,atts,cite,content = $~[1..4]
797 qtag,atts,cite,content = $~[1..4]
797 sta = ''
798 sta = ''
798 end
799 end
799 atts = pba( atts )
800 atts = pba( atts )
800 atts = shelve( atts ) if atts
801 atts = shelve( atts ) if atts
801
802
802 "#{ sta }#{ oqs }<#{ ht }#{ atts }>#{ content }</#{ ht }>#{ oqa }"
803 "#{ sta }#{ oqs }<#{ ht }#{ atts }>#{ content }</#{ ht }>#{ oqa }"
803
804
804 end
805 end
805 end
806 end
806 end
807 end
807
808
808 LINK_RE = /
809 LINK_RE = /
809 (
810 (
810 ([\s\[{(]|[#{PUNCT}])? # $pre
811 ([\s\[{(]|[#{PUNCT}])? # $pre
811 " # start
812 " # start
812 (#{C}) # $atts
813 (#{C}) # $atts
813 ([^"\n]+?) # $text
814 ([^"\n]+?) # $text
814 \s?
815 \s?
815 (?:\(([^)]+?)\)(?="))? # $title
816 (?:\(([^)]+?)\)(?="))? # $title
816 ":
817 ":
817 ( # $url
818 ( # $url
818 (\/|[a-zA-Z]+:\/\/|www\.|mailto:) # $proto
819 (\/|[a-zA-Z]+:\/\/|www\.|mailto:) # $proto
819 [[:alnum:]_\/]\S+?
820 [[:alnum:]_\/]\S+?
820 )
821 )
821 (\/)? # $slash
822 (\/)? # $slash
822 ([^[:alnum:]_\=\/;\(\)]*?) # $post
823 ([^[:alnum:]_\=\/;\(\)]*?) # $post
823 )
824 )
824 (?=<|\s|$)
825 (?=<|\s|$)
825 /x
826 /x
826 #"
827 #"
827 def inline_textile_link( text )
828 def inline_textile_link( text )
828 text.gsub!( LINK_RE ) do |m|
829 text.gsub!( LINK_RE ) do |m|
829 all,pre,atts,text,title,url,proto,slash,post = $~[1..9]
830 all,pre,atts,text,title,url,proto,slash,post = $~[1..9]
830 if text.include?('<br />')
831 if text.include?('<br />')
831 all
832 all
832 else
833 else
833 url, url_title = check_refs( url )
834 url, url_title = check_refs( url )
834 title ||= url_title
835 title ||= url_title
835
836
836 # Idea below : an URL with unbalanced parethesis and
837 # Idea below : an URL with unbalanced parethesis and
837 # ending by ')' is put into external parenthesis
838 # ending by ')' is put into external parenthesis
838 if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
839 if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
839 url=url[0..-2] # discard closing parenth from url
840 url=url[0..-2] # discard closing parenth from url
840 post = ")"+post # add closing parenth to post
841 post = ")"+post # add closing parenth to post
841 end
842 end
842 atts = pba( atts )
843 atts = pba( atts )
843 atts = " href=\"#{ htmlesc url }#{ slash }\"#{ atts }"
844 atts = " href=\"#{ htmlesc url }#{ slash }\"#{ atts }"
844 atts << " title=\"#{ htmlesc title }\"" if title
845 atts << " title=\"#{ htmlesc title }\"" if title
845 atts = shelve( atts ) if atts
846 atts = shelve( atts ) if atts
846
847
847 external = (url =~ /^https?:\/\//) ? ' class="external"' : ''
848 external = (url =~ /^https?:\/\//) ? ' class="external"' : ''
848
849
849 "#{ pre }<a#{ atts }#{ external }>#{ text }</a>#{ post }"
850 "#{ pre }<a#{ atts }#{ external }>#{ text }</a>#{ post }"
850 end
851 end
851 end
852 end
852 end
853 end
853
854
854 MARKDOWN_REFLINK_RE = /
855 MARKDOWN_REFLINK_RE = /
855 \[([^\[\]]+)\] # $text
856 \[([^\[\]]+)\] # $text
856 [ ]? # opt. space
857 [ ]? # opt. space
857 (?:\n[ ]*)? # one optional newline followed by spaces
858 (?:\n[ ]*)? # one optional newline followed by spaces
858 \[(.*?)\] # $id
859 \[(.*?)\] # $id
859 /x
860 /x
860
861
861 def inline_markdown_reflink( text )
862 def inline_markdown_reflink( text )
862 text.gsub!( MARKDOWN_REFLINK_RE ) do |m|
863 text.gsub!( MARKDOWN_REFLINK_RE ) do |m|
863 text, id = $~[1..2]
864 text, id = $~[1..2]
864
865
865 if id.empty?
866 if id.empty?
866 url, title = check_refs( text )
867 url, title = check_refs( text )
867 else
868 else
868 url, title = check_refs( id )
869 url, title = check_refs( id )
869 end
870 end
870
871
871 atts = " href=\"#{ url }\""
872 atts = " href=\"#{ url }\""
872 atts << " title=\"#{ title }\"" if title
873 atts << " title=\"#{ title }\"" if title
873 atts = shelve( atts )
874 atts = shelve( atts )
874
875
875 "<a#{ atts }>#{ text }</a>"
876 "<a#{ atts }>#{ text }</a>"
876 end
877 end
877 end
878 end
878
879
879 MARKDOWN_LINK_RE = /
880 MARKDOWN_LINK_RE = /
880 \[([^\[\]]+)\] # $text
881 \[([^\[\]]+)\] # $text
881 \( # open paren
882 \( # open paren
882 [ \t]* # opt space
883 [ \t]* # opt space
883 <?(.+?)>? # $href
884 <?(.+?)>? # $href
884 [ \t]* # opt space
885 [ \t]* # opt space
885 (?: # whole title
886 (?: # whole title
886 (['"]) # $quote
887 (['"]) # $quote
887 (.*?) # $title
888 (.*?) # $title
888 \3 # matching quote
889 \3 # matching quote
889 )? # title is optional
890 )? # title is optional
890 \)
891 \)
891 /x
892 /x
892
893
893 def inline_markdown_link( text )
894 def inline_markdown_link( text )
894 text.gsub!( MARKDOWN_LINK_RE ) do |m|
895 text.gsub!( MARKDOWN_LINK_RE ) do |m|
895 text, url, quote, title = $~[1..4]
896 text, url, quote, title = $~[1..4]
896
897
897 atts = " href=\"#{ url }\""
898 atts = " href=\"#{ url }\""
898 atts << " title=\"#{ title }\"" if title
899 atts << " title=\"#{ title }\"" if title
899 atts = shelve( atts )
900 atts = shelve( atts )
900
901
901 "<a#{ atts }>#{ text }</a>"
902 "<a#{ atts }>#{ text }</a>"
902 end
903 end
903 end
904 end
904
905
905 TEXTILE_REFS_RE = /(^ *)\[([^\[\n]+?)\](#{HYPERLINK})(?=\s|$)/
906 TEXTILE_REFS_RE = /(^ *)\[([^\[\n]+?)\](#{HYPERLINK})(?=\s|$)/
906 MARKDOWN_REFS_RE = /(^ *)\[([^\n]+?)\]:\s+<?(#{HYPERLINK})>?(?:\s+"((?:[^"]|\\")+)")?(?=\s|$)/m
907 MARKDOWN_REFS_RE = /(^ *)\[([^\n]+?)\]:\s+<?(#{HYPERLINK})>?(?:\s+"((?:[^"]|\\")+)")?(?=\s|$)/m
907
908
908 def refs( text )
909 def refs( text )
909 @rules.each do |rule_name|
910 @rules.each do |rule_name|
910 method( rule_name ).call( text ) if rule_name.to_s.match /^refs_/
911 method( rule_name ).call( text ) if rule_name.to_s.match /^refs_/
911 end
912 end
912 end
913 end
913
914
914 def refs_textile( text )
915 def refs_textile( text )
915 text.gsub!( TEXTILE_REFS_RE ) do |m|
916 text.gsub!( TEXTILE_REFS_RE ) do |m|
916 flag, url = $~[2..3]
917 flag, url = $~[2..3]
917 @urlrefs[flag.downcase] = [url, nil]
918 @urlrefs[flag.downcase] = [url, nil]
918 nil
919 nil
919 end
920 end
920 end
921 end
921
922
922 def refs_markdown( text )
923 def refs_markdown( text )
923 text.gsub!( MARKDOWN_REFS_RE ) do |m|
924 text.gsub!( MARKDOWN_REFS_RE ) do |m|
924 flag, url = $~[2..3]
925 flag, url = $~[2..3]
925 title = $~[6]
926 title = $~[6]
926 @urlrefs[flag.downcase] = [url, title]
927 @urlrefs[flag.downcase] = [url, title]
927 nil
928 nil
928 end
929 end
929 end
930 end
930
931
931 def check_refs( text )
932 def check_refs( text )
932 ret = @urlrefs[text.downcase] if text
933 ret = @urlrefs[text.downcase] if text
933 ret || [text, nil]
934 ret || [text, nil]
934 end
935 end
935
936
936 IMAGE_RE = /
937 IMAGE_RE = /
937 (>|\s|^) # start of line?
938 (>|\s|^) # start of line?
938 \! # opening
939 \! # opening
939 (\<|\=|\>)? # optional alignment atts
940 (\<|\=|\>)? # optional alignment atts
940 (#{C}) # optional style,class atts
941 (#{C}) # optional style,class atts
941 (?:\. )? # optional dot-space
942 (?:\. )? # optional dot-space
942 ([^\s(!]+?) # presume this is the src
943 ([^\s(!]+?) # presume this is the src
943 \s? # optional space
944 \s? # optional space
944 (?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title
945 (?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title
945 \! # closing
946 \! # closing
946 (?::#{ HYPERLINK })? # optional href
947 (?::#{ HYPERLINK })? # optional href
947 /x
948 /x
948
949
949 def inline_textile_image( text )
950 def inline_textile_image( text )
950 text.gsub!( IMAGE_RE ) do |m|
951 text.gsub!( IMAGE_RE ) do |m|
951 stln,algn,atts,url,title,href,href_a1,href_a2 = $~[1..8]
952 stln,algn,atts,url,title,href,href_a1,href_a2 = $~[1..8]
952 htmlesc title
953 htmlesc title
953 atts = pba( atts )
954 atts = pba( atts )
954 atts = " src=\"#{ htmlesc url.dup }\"#{ atts }"
955 atts = " src=\"#{ htmlesc url.dup }\"#{ atts }"
955 atts << " title=\"#{ title }\"" if title
956 atts << " title=\"#{ title }\"" if title
956 atts << " alt=\"#{ title }\""
957 atts << " alt=\"#{ title }\""
957 # size = @getimagesize($url);
958 # size = @getimagesize($url);
958 # if($size) $atts.= " $size[3]";
959 # if($size) $atts.= " $size[3]";
959
960
960 href, alt_title = check_refs( href ) if href
961 href, alt_title = check_refs( href ) if href
961 url, url_title = check_refs( url )
962 url, url_title = check_refs( url )
962
963
964 return m unless uri_with_safe_scheme?(url)
965
963 out = ''
966 out = ''
964 out << "<a#{ shelve( " href=\"#{ href }\"" ) }>" if href
967 out << "<a#{ shelve( " href=\"#{ href }\"" ) }>" if href
965 out << "<img#{ shelve( atts ) } />"
968 out << "<img#{ shelve( atts ) } />"
966 out << "</a>#{ href_a1 }#{ href_a2 }" if href
969 out << "</a>#{ href_a1 }#{ href_a2 }" if href
967
970
968 if algn
971 if algn
969 algn = h_align( algn )
972 algn = h_align( algn )
970 if stln == "<p>"
973 if stln == "<p>"
971 out = "<p style=\"float:#{ algn }\">#{ out }"
974 out = "<p style=\"float:#{ algn }\">#{ out }"
972 else
975 else
973 out = "#{ stln }<div style=\"float:#{ algn }\">#{ out }</div>"
976 out = "#{ stln }<div style=\"float:#{ algn }\">#{ out }</div>"
974 end
977 end
975 else
978 else
976 out = stln + out
979 out = stln + out
977 end
980 end
978
981
979 out
982 out
980 end
983 end
981 end
984 end
982
985
983 def shelve( val )
986 def shelve( val )
984 @shelf << val
987 @shelf << val
985 " :redsh##{ @shelf.length }:"
988 " :redsh##{ @shelf.length }:"
986 end
989 end
987
990
988 def retrieve( text )
991 def retrieve( text )
989 text.gsub!(/ :redsh#(\d+):/) do
992 text.gsub!(/ :redsh#(\d+):/) do
990 @shelf[$1.to_i - 1] || $&
993 @shelf[$1.to_i - 1] || $&
991 end
994 end
992 end
995 end
993
996
994 def incoming_entities( text )
997 def incoming_entities( text )
995 ## turn any incoming ampersands into a dummy character for now.
998 ## turn any incoming ampersands into a dummy character for now.
996 ## This uses a negative lookahead for alphanumerics followed by a semicolon,
999 ## This uses a negative lookahead for alphanumerics followed by a semicolon,
997 ## implying an incoming html entity, to be skipped
1000 ## implying an incoming html entity, to be skipped
998
1001
999 text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" )
1002 text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" )
1000 end
1003 end
1001
1004
1002 def no_textile( text )
1005 def no_textile( text )
1003 text.gsub!( /(^|\s)==([^=]+.*?)==(\s|$)?/,
1006 text.gsub!( /(^|\s)==([^=]+.*?)==(\s|$)?/,
1004 '\1<notextile>\2</notextile>\3' )
1007 '\1<notextile>\2</notextile>\3' )
1005 text.gsub!( /^ *==([^=]+.*?)==/m,
1008 text.gsub!( /^ *==([^=]+.*?)==/m,
1006 '\1<notextile>\2</notextile>\3' )
1009 '\1<notextile>\2</notextile>\3' )
1007 end
1010 end
1008
1011
1009 def clean_white_space( text )
1012 def clean_white_space( text )
1010 # normalize line breaks
1013 # normalize line breaks
1011 text.gsub!( /\r\n/, "\n" )
1014 text.gsub!( /\r\n/, "\n" )
1012 text.gsub!( /\r/, "\n" )
1015 text.gsub!( /\r/, "\n" )
1013 text.gsub!( /\t/, ' ' )
1016 text.gsub!( /\t/, ' ' )
1014 text.gsub!( /^ +$/, '' )
1017 text.gsub!( /^ +$/, '' )
1015 text.gsub!( /\n{3,}/, "\n\n" )
1018 text.gsub!( /\n{3,}/, "\n\n" )
1016 text.gsub!( /"$/, "\" " )
1019 text.gsub!( /"$/, "\" " )
1017
1020
1018 # if entire document is indented, flush
1021 # if entire document is indented, flush
1019 # to the left side
1022 # to the left side
1020 flush_left text
1023 flush_left text
1021 end
1024 end
1022
1025
1023 def flush_left( text )
1026 def flush_left( text )
1024 indt = 0
1027 indt = 0
1025 if text =~ /^ /
1028 if text =~ /^ /
1026 while text !~ /^ {#{indt}}\S/
1029 while text !~ /^ {#{indt}}\S/
1027 indt += 1
1030 indt += 1
1028 end unless text.empty?
1031 end unless text.empty?
1029 if indt.nonzero?
1032 if indt.nonzero?
1030 text.gsub!( /^ {#{indt}}/, '' )
1033 text.gsub!( /^ {#{indt}}/, '' )
1031 end
1034 end
1032 end
1035 end
1033 end
1036 end
1034
1037
1035 def footnote_ref( text )
1038 def footnote_ref( text )
1036 text.gsub!( /\b\[([0-9]+?)\](\s)?/,
1039 text.gsub!( /\b\[([0-9]+?)\](\s)?/,
1037 '<sup><a href="#fn\1">\1</a></sup>\2' )
1040 '<sup><a href="#fn\1">\1</a></sup>\2' )
1038 end
1041 end
1039
1042
1040 OFFTAGS = /(code|pre|kbd|notextile)/
1043 OFFTAGS = /(code|pre|kbd|notextile)/
1041 OFFTAG_MATCH = /(?:(<\/#{ OFFTAGS }>)|(<#{ OFFTAGS }[^>]*>))(.*?)(?=<\/?#{ OFFTAGS }\W|\Z)/mi
1044 OFFTAG_MATCH = /(?:(<\/#{ OFFTAGS }>)|(<#{ OFFTAGS }[^>]*>))(.*?)(?=<\/?#{ OFFTAGS }\W|\Z)/mi
1042 OFFTAG_OPEN = /<#{ OFFTAGS }/
1045 OFFTAG_OPEN = /<#{ OFFTAGS }/
1043 OFFTAG_CLOSE = /<\/?#{ OFFTAGS }/
1046 OFFTAG_CLOSE = /<\/?#{ OFFTAGS }/
1044 HASTAG_MATCH = /(<\/?\w[^\n]*?>)/m
1047 HASTAG_MATCH = /(<\/?\w[^\n]*?>)/m
1045 ALLTAG_MATCH = /(<\/?\w[^\n]*?>)|.*?(?=<\/?\w[^\n]*?>|$)/m
1048 ALLTAG_MATCH = /(<\/?\w[^\n]*?>)|.*?(?=<\/?\w[^\n]*?>|$)/m
1046
1049
1047 def glyphs_textile( text, level = 0 )
1050 def glyphs_textile( text, level = 0 )
1048 if text !~ HASTAG_MATCH
1051 if text !~ HASTAG_MATCH
1049 pgl text
1052 pgl text
1050 footnote_ref text
1053 footnote_ref text
1051 else
1054 else
1052 codepre = 0
1055 codepre = 0
1053 text.gsub!( ALLTAG_MATCH ) do |line|
1056 text.gsub!( ALLTAG_MATCH ) do |line|
1054 ## matches are off if we're between <code>, <pre> etc.
1057 ## matches are off if we're between <code>, <pre> etc.
1055 if $1
1058 if $1
1056 if line =~ OFFTAG_OPEN
1059 if line =~ OFFTAG_OPEN
1057 codepre += 1
1060 codepre += 1
1058 elsif line =~ OFFTAG_CLOSE
1061 elsif line =~ OFFTAG_CLOSE
1059 codepre -= 1
1062 codepre -= 1
1060 codepre = 0 if codepre < 0
1063 codepre = 0 if codepre < 0
1061 end
1064 end
1062 elsif codepre.zero?
1065 elsif codepre.zero?
1063 glyphs_textile( line, level + 1 )
1066 glyphs_textile( line, level + 1 )
1064 else
1067 else
1065 htmlesc( line, :NoQuotes )
1068 htmlesc( line, :NoQuotes )
1066 end
1069 end
1067 # p [level, codepre, line]
1070 # p [level, codepre, line]
1068
1071
1069 line
1072 line
1070 end
1073 end
1071 end
1074 end
1072 end
1075 end
1073
1076
1074 def rip_offtags( text, escape_aftertag=true, escape_line=true )
1077 def rip_offtags( text, escape_aftertag=true, escape_line=true )
1075 if text =~ /<.*>/
1078 if text =~ /<.*>/
1076 ## strip and encode <pre> content
1079 ## strip and encode <pre> content
1077 codepre, used_offtags = 0, {}
1080 codepre, used_offtags = 0, {}
1078 text.gsub!( OFFTAG_MATCH ) do |line|
1081 text.gsub!( OFFTAG_MATCH ) do |line|
1079 if $3
1082 if $3
1080 first, offtag, aftertag = $3, $4, $5
1083 first, offtag, aftertag = $3, $4, $5
1081 codepre += 1
1084 codepre += 1
1082 used_offtags[offtag] = true
1085 used_offtags[offtag] = true
1083 if codepre - used_offtags.length > 0
1086 if codepre - used_offtags.length > 0
1084 htmlesc( line, :NoQuotes ) if escape_line
1087 htmlesc( line, :NoQuotes ) if escape_line
1085 @pre_list.last << line
1088 @pre_list.last << line
1086 line = ""
1089 line = ""
1087 else
1090 else
1088 ### htmlesc is disabled between CODE tags which will be parsed with highlighter
1091 ### htmlesc is disabled between CODE tags which will be parsed with highlighter
1089 ### Regexp in formatter.rb is : /<code\s+class="(\w+)">\s?(.+)/m
1092 ### Regexp in formatter.rb is : /<code\s+class="(\w+)">\s?(.+)/m
1090 ### NB: some changes were made not to use $N variables, because we use "match"
1093 ### NB: some changes were made not to use $N variables, because we use "match"
1091 ### and it breaks following lines
1094 ### and it breaks following lines
1092 htmlesc( aftertag, :NoQuotes ) if aftertag && escape_aftertag && !first.match(/<code\s+class="(\w+)">/)
1095 htmlesc( aftertag, :NoQuotes ) if aftertag && escape_aftertag && !first.match(/<code\s+class="(\w+)">/)
1093 line = "<redpre##{ @pre_list.length }>"
1096 line = "<redpre##{ @pre_list.length }>"
1094 first.match(/<#{ OFFTAGS }([^>]*)>/)
1097 first.match(/<#{ OFFTAGS }([^>]*)>/)
1095 tag = $1
1098 tag = $1
1096 $2.to_s.match(/(class\=("[^"]+"|'[^']+'))/i)
1099 $2.to_s.match(/(class\=("[^"]+"|'[^']+'))/i)
1097 tag << " #{$1}" if $1
1100 tag << " #{$1}" if $1
1098 @pre_list << "<#{ tag }>#{ aftertag }"
1101 @pre_list << "<#{ tag }>#{ aftertag }"
1099 end
1102 end
1100 elsif $1 and codepre > 0
1103 elsif $1 and codepre > 0
1101 if codepre - used_offtags.length > 0
1104 if codepre - used_offtags.length > 0
1102 htmlesc( line, :NoQuotes ) if escape_line
1105 htmlesc( line, :NoQuotes ) if escape_line
1103 @pre_list.last << line
1106 @pre_list.last << line
1104 line = ""
1107 line = ""
1105 end
1108 end
1106 codepre -= 1 unless codepre.zero?
1109 codepre -= 1 unless codepre.zero?
1107 used_offtags = {} if codepre.zero?
1110 used_offtags = {} if codepre.zero?
1108 end
1111 end
1109 line
1112 line
1110 end
1113 end
1111 end
1114 end
1112 text
1115 text
1113 end
1116 end
1114
1117
1115 def smooth_offtags( text )
1118 def smooth_offtags( text )
1116 unless @pre_list.empty?
1119 unless @pre_list.empty?
1117 ## replace <pre> content
1120 ## replace <pre> content
1118 text.gsub!( /<redpre#(\d+)>/ ) { @pre_list[$1.to_i] }
1121 text.gsub!( /<redpre#(\d+)>/ ) { @pre_list[$1.to_i] }
1119 end
1122 end
1120 end
1123 end
1121
1124
1122 def inline( text )
1125 def inline( text )
1123 [/^inline_/, /^glyphs_/].each do |meth_re|
1126 [/^inline_/, /^glyphs_/].each do |meth_re|
1124 @rules.each do |rule_name|
1127 @rules.each do |rule_name|
1125 method( rule_name ).call( text ) if rule_name.to_s.match( meth_re )
1128 method( rule_name ).call( text ) if rule_name.to_s.match( meth_re )
1126 end
1129 end
1127 end
1130 end
1128 end
1131 end
1129
1132
1130 def h_align( text )
1133 def h_align( text )
1131 H_ALGN_VALS[text]
1134 H_ALGN_VALS[text]
1132 end
1135 end
1133
1136
1134 def v_align( text )
1137 def v_align( text )
1135 V_ALGN_VALS[text]
1138 V_ALGN_VALS[text]
1136 end
1139 end
1137
1140
1138 def textile_popup_help( name, windowW, windowH )
1141 def textile_popup_help( name, windowW, windowH )
1139 ' <a target="_blank" href="http://hobix.com/textile/#' + helpvar + '" onclick="window.open(this.href, \'popupwindow\', \'width=' + windowW + ',height=' + windowH + ',scrollbars,resizable\'); return false;">' + name + '</a><br />'
1142 ' <a target="_blank" href="http://hobix.com/textile/#' + helpvar + '" onclick="window.open(this.href, \'popupwindow\', \'width=' + windowW + ',height=' + windowH + ',scrollbars,resizable\'); return false;">' + name + '</a><br />'
1140 end
1143 end
1141
1144
1142 # HTML cleansing stuff
1145 # HTML cleansing stuff
1143 BASIC_TAGS = {
1146 BASIC_TAGS = {
1144 'a' => ['href', 'title'],
1147 'a' => ['href', 'title'],
1145 'img' => ['src', 'alt', 'title'],
1148 'img' => ['src', 'alt', 'title'],
1146 'br' => [],
1149 'br' => [],
1147 'i' => nil,
1150 'i' => nil,
1148 'u' => nil,
1151 'u' => nil,
1149 'b' => nil,
1152 'b' => nil,
1150 'pre' => nil,
1153 'pre' => nil,
1151 'kbd' => nil,
1154 'kbd' => nil,
1152 'code' => ['lang'],
1155 'code' => ['lang'],
1153 'cite' => nil,
1156 'cite' => nil,
1154 'strong' => nil,
1157 'strong' => nil,
1155 'em' => nil,
1158 'em' => nil,
1156 'ins' => nil,
1159 'ins' => nil,
1157 'sup' => nil,
1160 'sup' => nil,
1158 'sub' => nil,
1161 'sub' => nil,
1159 'del' => nil,
1162 'del' => nil,
1160 'table' => nil,
1163 'table' => nil,
1161 'tr' => nil,
1164 'tr' => nil,
1162 'td' => ['colspan', 'rowspan'],
1165 'td' => ['colspan', 'rowspan'],
1163 'th' => nil,
1166 'th' => nil,
1164 'ol' => nil,
1167 'ol' => nil,
1165 'ul' => nil,
1168 'ul' => nil,
1166 'li' => nil,
1169 'li' => nil,
1167 'p' => nil,
1170 'p' => nil,
1168 'h1' => nil,
1171 'h1' => nil,
1169 'h2' => nil,
1172 'h2' => nil,
1170 'h3' => nil,
1173 'h3' => nil,
1171 'h4' => nil,
1174 'h4' => nil,
1172 'h5' => nil,
1175 'h5' => nil,
1173 'h6' => nil,
1176 'h6' => nil,
1174 'blockquote' => ['cite']
1177 'blockquote' => ['cite']
1175 }
1178 }
1176
1179
1177 def clean_html( text, tags = BASIC_TAGS )
1180 def clean_html( text, tags = BASIC_TAGS )
1178 text.gsub!( /<!\[CDATA\[/, '' )
1181 text.gsub!( /<!\[CDATA\[/, '' )
1179 text.gsub!( /<(\/*)(\w+)([^>]*)>/ ) do
1182 text.gsub!( /<(\/*)(\w+)([^>]*)>/ ) do
1180 raw = $~
1183 raw = $~
1181 tag = raw[2].downcase
1184 tag = raw[2].downcase
1182 if tags.has_key? tag
1185 if tags.has_key? tag
1183 pcs = [tag]
1186 pcs = [tag]
1184 tags[tag].each do |prop|
1187 tags[tag].each do |prop|
1185 ['"', "'", ''].each do |q|
1188 ['"', "'", ''].each do |q|
1186 q2 = ( q != '' ? q : '\s' )
1189 q2 = ( q != '' ? q : '\s' )
1187 if raw[3] =~ /#{prop}\s*=\s*#{q}([^#{q2}]+)#{q}/i
1190 if raw[3] =~ /#{prop}\s*=\s*#{q}([^#{q2}]+)#{q}/i
1188 attrv = $1
1191 attrv = $1
1189 next if prop == 'src' and attrv =~ %r{^(?!http)\w+:}
1192 next if prop == 'src' and attrv =~ %r{^(?!http)\w+:}
1190 pcs << "#{prop}=\"#{$1.gsub('"', '\\"')}\""
1193 pcs << "#{prop}=\"#{$1.gsub('"', '\\"')}\""
1191 break
1194 break
1192 end
1195 end
1193 end
1196 end
1194 end if tags[tag]
1197 end if tags[tag]
1195 "<#{raw[1]}#{pcs.join " "}>"
1198 "<#{raw[1]}#{pcs.join " "}>"
1196 else
1199 else
1197 " "
1200 " "
1198 end
1201 end
1199 end
1202 end
1200 end
1203 end
1201
1204
1202 ALLOWED_TAGS = %w(redpre pre code notextile)
1205 ALLOWED_TAGS = %w(redpre pre code notextile)
1203
1206
1204 def escape_html_tags(text)
1207 def escape_html_tags(text)
1205 text.gsub!(%r{<(\/?([!\w]+)[^<>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "&lt;#{$1}#{'&gt;' unless $3.blank?}" }
1208 text.gsub!(%r{<(\/?([!\w]+)[^<>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "&lt;#{$1}#{'&gt;' unless $3.blank?}" }
1206 end
1209 end
1207 end
1210 end
1208
1211
@@ -1,805 +1,818
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 module Redmine
18 module Redmine
19 module FieldFormat
19 module FieldFormat
20 def self.add(name, klass)
20 def self.add(name, klass)
21 all[name.to_s] = klass.instance
21 all[name.to_s] = klass.instance
22 end
22 end
23
23
24 def self.delete(name)
24 def self.delete(name)
25 all.delete(name.to_s)
25 all.delete(name.to_s)
26 end
26 end
27
27
28 def self.all
28 def self.all
29 @formats ||= Hash.new(Base.instance)
29 @formats ||= Hash.new(Base.instance)
30 end
30 end
31
31
32 def self.available_formats
32 def self.available_formats
33 all.keys
33 all.keys
34 end
34 end
35
35
36 def self.find(name)
36 def self.find(name)
37 all[name.to_s]
37 all[name.to_s]
38 end
38 end
39
39
40 # Return an array of custom field formats which can be used in select_tag
40 # Return an array of custom field formats which can be used in select_tag
41 def self.as_select(class_name=nil)
41 def self.as_select(class_name=nil)
42 formats = all.values.select do |format|
42 formats = all.values.select do |format|
43 format.class.customized_class_names.nil? || format.class.customized_class_names.include?(class_name)
43 format.class.customized_class_names.nil? || format.class.customized_class_names.include?(class_name)
44 end
44 end
45 formats.map {|format| [::I18n.t(format.label), format.name] }.sort_by(&:first)
45 formats.map {|format| [::I18n.t(format.label), format.name] }.sort_by(&:first)
46 end
46 end
47
47
48 class Base
48 class Base
49 include Singleton
49 include Singleton
50 include Redmine::I18n
50 include Redmine::I18n
51 include Redmine::Helpers::URL
51 include ERB::Util
52 include ERB::Util
52
53
53 class_attribute :format_name
54 class_attribute :format_name
54 self.format_name = nil
55 self.format_name = nil
55
56
56 # Set this to true if the format supports multiple values
57 # Set this to true if the format supports multiple values
57 class_attribute :multiple_supported
58 class_attribute :multiple_supported
58 self.multiple_supported = false
59 self.multiple_supported = false
59
60
60 # Set this to true if the format supports textual search on custom values
61 # Set this to true if the format supports textual search on custom values
61 class_attribute :searchable_supported
62 class_attribute :searchable_supported
62 self.searchable_supported = false
63 self.searchable_supported = false
63
64
64 # Set this to true if field values can be summed up
65 # Set this to true if field values can be summed up
65 class_attribute :totalable_supported
66 class_attribute :totalable_supported
66 self.totalable_supported = false
67 self.totalable_supported = false
67
68
68 # Restricts the classes that the custom field can be added to
69 # Restricts the classes that the custom field can be added to
69 # Set to nil for no restrictions
70 # Set to nil for no restrictions
70 class_attribute :customized_class_names
71 class_attribute :customized_class_names
71 self.customized_class_names = nil
72 self.customized_class_names = nil
72
73
73 # Name of the partial for editing the custom field
74 # Name of the partial for editing the custom field
74 class_attribute :form_partial
75 class_attribute :form_partial
75 self.form_partial = nil
76 self.form_partial = nil
76
77
77 class_attribute :change_as_diff
78 class_attribute :change_as_diff
78 self.change_as_diff = false
79 self.change_as_diff = false
79
80
80 def self.add(name)
81 def self.add(name)
81 self.format_name = name
82 self.format_name = name
82 Redmine::FieldFormat.add(name, self)
83 Redmine::FieldFormat.add(name, self)
83 end
84 end
84 private_class_method :add
85 private_class_method :add
85
86
86 def self.field_attributes(*args)
87 def self.field_attributes(*args)
87 CustomField.store_accessor :format_store, *args
88 CustomField.store_accessor :format_store, *args
88 end
89 end
89
90
90 field_attributes :url_pattern
91 field_attributes :url_pattern
91
92
92 def name
93 def name
93 self.class.format_name
94 self.class.format_name
94 end
95 end
95
96
96 def label
97 def label
97 "label_#{name}"
98 "label_#{name}"
98 end
99 end
99
100
100 def cast_custom_value(custom_value)
101 def cast_custom_value(custom_value)
101 cast_value(custom_value.custom_field, custom_value.value, custom_value.customized)
102 cast_value(custom_value.custom_field, custom_value.value, custom_value.customized)
102 end
103 end
103
104
104 def cast_value(custom_field, value, customized=nil)
105 def cast_value(custom_field, value, customized=nil)
105 if value.blank?
106 if value.blank?
106 nil
107 nil
107 elsif value.is_a?(Array)
108 elsif value.is_a?(Array)
108 casted = value.map do |v|
109 casted = value.map do |v|
109 cast_single_value(custom_field, v, customized)
110 cast_single_value(custom_field, v, customized)
110 end
111 end
111 casted.compact.sort
112 casted.compact.sort
112 else
113 else
113 cast_single_value(custom_field, value, customized)
114 cast_single_value(custom_field, value, customized)
114 end
115 end
115 end
116 end
116
117
117 def cast_single_value(custom_field, value, customized=nil)
118 def cast_single_value(custom_field, value, customized=nil)
118 value.to_s
119 value.to_s
119 end
120 end
120
121
121 def target_class
122 def target_class
122 nil
123 nil
123 end
124 end
124
125
125 def possible_custom_value_options(custom_value)
126 def possible_custom_value_options(custom_value)
126 possible_values_options(custom_value.custom_field, custom_value.customized)
127 possible_values_options(custom_value.custom_field, custom_value.customized)
127 end
128 end
128
129
129 def possible_values_options(custom_field, object=nil)
130 def possible_values_options(custom_field, object=nil)
130 []
131 []
131 end
132 end
132
133
133 def value_from_keyword(custom_field, keyword, object)
134 def value_from_keyword(custom_field, keyword, object)
134 possible_values_options = possible_values_options(custom_field, object)
135 possible_values_options = possible_values_options(custom_field, object)
135 if possible_values_options.present?
136 if possible_values_options.present?
136 keyword = keyword.to_s
137 keyword = keyword.to_s
137 if v = possible_values_options.detect {|text, id| keyword.casecmp(text) == 0}
138 if v = possible_values_options.detect {|text, id| keyword.casecmp(text) == 0}
138 if v.is_a?(Array)
139 if v.is_a?(Array)
139 v.last
140 v.last
140 else
141 else
141 v
142 v
142 end
143 end
143 end
144 end
144 else
145 else
145 keyword
146 keyword
146 end
147 end
147 end
148 end
148
149
149 # Returns the validation errors for custom_field
150 # Returns the validation errors for custom_field
150 # Should return an empty array if custom_field is valid
151 # Should return an empty array if custom_field is valid
151 def validate_custom_field(custom_field)
152 def validate_custom_field(custom_field)
152 []
153 errors = []
154 pattern = custom_field.url_pattern
155 if pattern.present? && !uri_with_safe_scheme?(url_pattern_without_tokens(pattern))
156 errors << [:url_pattern, :invalid]
157 end
158 errors
153 end
159 end
154
160
155 # Returns the validation error messages for custom_value
161 # Returns the validation error messages for custom_value
156 # Should return an empty array if custom_value is valid
162 # Should return an empty array if custom_value is valid
157 def validate_custom_value(custom_value)
163 def validate_custom_value(custom_value)
158 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
164 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
159 errors = values.map do |value|
165 errors = values.map do |value|
160 validate_single_value(custom_value.custom_field, value, custom_value.customized)
166 validate_single_value(custom_value.custom_field, value, custom_value.customized)
161 end
167 end
162 errors.flatten.uniq
168 errors.flatten.uniq
163 end
169 end
164
170
165 def validate_single_value(custom_field, value, customized=nil)
171 def validate_single_value(custom_field, value, customized=nil)
166 []
172 []
167 end
173 end
168
174
169 def formatted_custom_value(view, custom_value, html=false)
175 def formatted_custom_value(view, custom_value, html=false)
170 formatted_value(view, custom_value.custom_field, custom_value.value, custom_value.customized, html)
176 formatted_value(view, custom_value.custom_field, custom_value.value, custom_value.customized, html)
171 end
177 end
172
178
173 def formatted_value(view, custom_field, value, customized=nil, html=false)
179 def formatted_value(view, custom_field, value, customized=nil, html=false)
174 casted = cast_value(custom_field, value, customized)
180 casted = cast_value(custom_field, value, customized)
175 if html && custom_field.url_pattern.present?
181 if html && custom_field.url_pattern.present?
176 texts_and_urls = Array.wrap(casted).map do |single_value|
182 texts_and_urls = Array.wrap(casted).map do |single_value|
177 text = view.format_object(single_value, false).to_s
183 text = view.format_object(single_value, false).to_s
178 url = url_from_pattern(custom_field, single_value, customized)
184 url = url_from_pattern(custom_field, single_value, customized)
179 [text, url]
185 [text, url]
180 end
186 end
181 links = texts_and_urls.sort_by(&:first).map {|text, url| view.link_to text, url}
187 links = texts_and_urls.sort_by(&:first).map {|text, url| view.link_to_if uri_with_safe_scheme?(url), text, url}
182 links.join(', ').html_safe
188 links.join(', ').html_safe
183 else
189 else
184 casted
190 casted
185 end
191 end
186 end
192 end
187
193
188 # Returns an URL generated with the custom field URL pattern
194 # Returns an URL generated with the custom field URL pattern
189 # and variables substitution:
195 # and variables substitution:
190 # %value% => the custom field value
196 # %value% => the custom field value
191 # %id% => id of the customized object
197 # %id% => id of the customized object
192 # %project_id% => id of the project of the customized object if defined
198 # %project_id% => id of the project of the customized object if defined
193 # %project_identifier% => identifier of the project of the customized object if defined
199 # %project_identifier% => identifier of the project of the customized object if defined
194 # %m1%, %m2%... => capture groups matches of the custom field regexp if defined
200 # %m1%, %m2%... => capture groups matches of the custom field regexp if defined
195 def url_from_pattern(custom_field, value, customized)
201 def url_from_pattern(custom_field, value, customized)
196 url = custom_field.url_pattern.to_s.dup
202 url = custom_field.url_pattern.to_s.dup
197 url.gsub!('%value%') {value.to_s}
203 url.gsub!('%value%') {value.to_s}
198 url.gsub!('%id%') {customized.id.to_s}
204 url.gsub!('%id%') {customized.id.to_s}
199 url.gsub!('%project_id%') {(customized.respond_to?(:project) ? customized.project.try(:id) : nil).to_s}
205 url.gsub!('%project_id%') {(customized.respond_to?(:project) ? customized.project.try(:id) : nil).to_s}
200 url.gsub!('%project_identifier%') {(customized.respond_to?(:project) ? customized.project.try(:identifier) : nil).to_s}
206 url.gsub!('%project_identifier%') {(customized.respond_to?(:project) ? customized.project.try(:identifier) : nil).to_s}
201 if custom_field.regexp.present?
207 if custom_field.regexp.present?
202 url.gsub!(%r{%m(\d+)%}) do
208 url.gsub!(%r{%m(\d+)%}) do
203 m = $1.to_i
209 m = $1.to_i
204 if matches ||= value.to_s.match(Regexp.new(custom_field.regexp))
210 if matches ||= value.to_s.match(Regexp.new(custom_field.regexp))
205 matches[m].to_s
211 matches[m].to_s
206 end
212 end
207 end
213 end
208 end
214 end
209 url
215 url
210 end
216 end
211 protected :url_from_pattern
217 protected :url_from_pattern
212
218
219 # Returns the URL pattern with substitution tokens removed,
220 # for validation purpose
221 def url_pattern_without_tokens(url_pattern)
222 url_pattern.to_s.gsub(/%(value|id|project_id|project_identifier|m\d+)%/, '')
223 end
224 protected :url_pattern_without_tokens
225
213 def edit_tag(view, tag_id, tag_name, custom_value, options={})
226 def edit_tag(view, tag_id, tag_name, custom_value, options={})
214 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id))
227 view.text_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id))
215 end
228 end
216
229
217 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
230 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
218 view.text_field_tag(tag_name, value, options.merge(:id => tag_id)) +
231 view.text_field_tag(tag_name, value, options.merge(:id => tag_id)) +
219 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
232 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
220 end
233 end
221
234
222 def bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
235 def bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
223 if custom_field.is_required?
236 if custom_field.is_required?
224 ''.html_safe
237 ''.html_safe
225 else
238 else
226 view.content_tag('label',
239 view.content_tag('label',
227 view.check_box_tag(tag_name, '__none__', (value == '__none__'), :id => nil, :data => {:disables => "##{tag_id}"}) + l(:button_clear),
240 view.check_box_tag(tag_name, '__none__', (value == '__none__'), :id => nil, :data => {:disables => "##{tag_id}"}) + l(:button_clear),
228 :class => 'inline'
241 :class => 'inline'
229 )
242 )
230 end
243 end
231 end
244 end
232 protected :bulk_clear_tag
245 protected :bulk_clear_tag
233
246
234 def query_filter_options(custom_field, query)
247 def query_filter_options(custom_field, query)
235 {:type => :string}
248 {:type => :string}
236 end
249 end
237
250
238 def before_custom_field_save(custom_field)
251 def before_custom_field_save(custom_field)
239 end
252 end
240
253
241 # Returns a ORDER BY clause that can used to sort customized
254 # Returns a ORDER BY clause that can used to sort customized
242 # objects by their value of the custom field.
255 # objects by their value of the custom field.
243 # Returns nil if the custom field can not be used for sorting.
256 # Returns nil if the custom field can not be used for sorting.
244 def order_statement(custom_field)
257 def order_statement(custom_field)
245 # COALESCE is here to make sure that blank and NULL values are sorted equally
258 # COALESCE is here to make sure that blank and NULL values are sorted equally
246 "COALESCE(#{join_alias custom_field}.value, '')"
259 "COALESCE(#{join_alias custom_field}.value, '')"
247 end
260 end
248
261
249 # Returns a GROUP BY clause that can used to group by custom value
262 # Returns a GROUP BY clause that can used to group by custom value
250 # Returns nil if the custom field can not be used for grouping.
263 # Returns nil if the custom field can not be used for grouping.
251 def group_statement(custom_field)
264 def group_statement(custom_field)
252 nil
265 nil
253 end
266 end
254
267
255 # Returns a JOIN clause that is added to the query when sorting by custom values
268 # Returns a JOIN clause that is added to the query when sorting by custom values
256 def join_for_order_statement(custom_field)
269 def join_for_order_statement(custom_field)
257 alias_name = join_alias(custom_field)
270 alias_name = join_alias(custom_field)
258
271
259 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
272 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
260 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
273 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
261 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
274 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
262 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
275 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
263 " AND (#{custom_field.visibility_by_project_condition})" +
276 " AND (#{custom_field.visibility_by_project_condition})" +
264 " AND #{alias_name}.value <> ''" +
277 " AND #{alias_name}.value <> ''" +
265 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
278 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
266 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
279 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
267 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
280 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
268 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)"
281 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)"
269 end
282 end
270
283
271 def join_alias(custom_field)
284 def join_alias(custom_field)
272 "cf_#{custom_field.id}"
285 "cf_#{custom_field.id}"
273 end
286 end
274 protected :join_alias
287 protected :join_alias
275 end
288 end
276
289
277 class Unbounded < Base
290 class Unbounded < Base
278 def validate_single_value(custom_field, value, customized=nil)
291 def validate_single_value(custom_field, value, customized=nil)
279 errs = super
292 errs = super
280 value = value.to_s
293 value = value.to_s
281 unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
294 unless custom_field.regexp.blank? or value =~ Regexp.new(custom_field.regexp)
282 errs << ::I18n.t('activerecord.errors.messages.invalid')
295 errs << ::I18n.t('activerecord.errors.messages.invalid')
283 end
296 end
284 if custom_field.min_length && value.length < custom_field.min_length
297 if custom_field.min_length && value.length < custom_field.min_length
285 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => custom_field.min_length)
298 errs << ::I18n.t('activerecord.errors.messages.too_short', :count => custom_field.min_length)
286 end
299 end
287 if custom_field.max_length && custom_field.max_length > 0 && value.length > custom_field.max_length
300 if custom_field.max_length && custom_field.max_length > 0 && value.length > custom_field.max_length
288 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => custom_field.max_length)
301 errs << ::I18n.t('activerecord.errors.messages.too_long', :count => custom_field.max_length)
289 end
302 end
290 errs
303 errs
291 end
304 end
292 end
305 end
293
306
294 class StringFormat < Unbounded
307 class StringFormat < Unbounded
295 add 'string'
308 add 'string'
296 self.searchable_supported = true
309 self.searchable_supported = true
297 self.form_partial = 'custom_fields/formats/string'
310 self.form_partial = 'custom_fields/formats/string'
298 field_attributes :text_formatting
311 field_attributes :text_formatting
299
312
300 def formatted_value(view, custom_field, value, customized=nil, html=false)
313 def formatted_value(view, custom_field, value, customized=nil, html=false)
301 if html
314 if html
302 if custom_field.url_pattern.present?
315 if custom_field.url_pattern.present?
303 super
316 super
304 elsif custom_field.text_formatting == 'full'
317 elsif custom_field.text_formatting == 'full'
305 view.textilizable(value, :object => customized)
318 view.textilizable(value, :object => customized)
306 else
319 else
307 value.to_s
320 value.to_s
308 end
321 end
309 else
322 else
310 value.to_s
323 value.to_s
311 end
324 end
312 end
325 end
313 end
326 end
314
327
315 class TextFormat < Unbounded
328 class TextFormat < Unbounded
316 add 'text'
329 add 'text'
317 self.searchable_supported = true
330 self.searchable_supported = true
318 self.form_partial = 'custom_fields/formats/text'
331 self.form_partial = 'custom_fields/formats/text'
319 self.change_as_diff = true
332 self.change_as_diff = true
320
333
321 def formatted_value(view, custom_field, value, customized=nil, html=false)
334 def formatted_value(view, custom_field, value, customized=nil, html=false)
322 if html
335 if html
323 if value.present?
336 if value.present?
324 if custom_field.text_formatting == 'full'
337 if custom_field.text_formatting == 'full'
325 view.textilizable(value, :object => customized)
338 view.textilizable(value, :object => customized)
326 else
339 else
327 view.simple_format(html_escape(value))
340 view.simple_format(html_escape(value))
328 end
341 end
329 else
342 else
330 ''
343 ''
331 end
344 end
332 else
345 else
333 value.to_s
346 value.to_s
334 end
347 end
335 end
348 end
336
349
337 def edit_tag(view, tag_id, tag_name, custom_value, options={})
350 def edit_tag(view, tag_id, tag_name, custom_value, options={})
338 view.text_area_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :rows => 3))
351 view.text_area_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :rows => 3))
339 end
352 end
340
353
341 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
354 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
342 view.text_area_tag(tag_name, value, options.merge(:id => tag_id, :rows => 3)) +
355 view.text_area_tag(tag_name, value, options.merge(:id => tag_id, :rows => 3)) +
343 '<br />'.html_safe +
356 '<br />'.html_safe +
344 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
357 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
345 end
358 end
346
359
347 def query_filter_options(custom_field, query)
360 def query_filter_options(custom_field, query)
348 {:type => :text}
361 {:type => :text}
349 end
362 end
350 end
363 end
351
364
352 class LinkFormat < StringFormat
365 class LinkFormat < StringFormat
353 add 'link'
366 add 'link'
354 self.searchable_supported = false
367 self.searchable_supported = false
355 self.form_partial = 'custom_fields/formats/link'
368 self.form_partial = 'custom_fields/formats/link'
356
369
357 def formatted_value(view, custom_field, value, customized=nil, html=false)
370 def formatted_value(view, custom_field, value, customized=nil, html=false)
358 if html && value.present?
371 if html && value.present?
359 if custom_field.url_pattern.present?
372 if custom_field.url_pattern.present?
360 url = url_from_pattern(custom_field, value, customized)
373 url = url_from_pattern(custom_field, value, customized)
361 else
374 else
362 url = value.to_s
375 url = value.to_s
363 unless url =~ %r{\A[a-z]+://}i
376 unless url =~ %r{\A[a-z]+://}i
364 # no protocol found, use http by default
377 # no protocol found, use http by default
365 url = "http://" + url
378 url = "http://" + url
366 end
379 end
367 end
380 end
368 view.link_to value.to_s.truncate(40), url
381 view.link_to value.to_s.truncate(40), url
369 else
382 else
370 value.to_s
383 value.to_s
371 end
384 end
372 end
385 end
373 end
386 end
374
387
375 class Numeric < Unbounded
388 class Numeric < Unbounded
376 self.form_partial = 'custom_fields/formats/numeric'
389 self.form_partial = 'custom_fields/formats/numeric'
377 self.totalable_supported = true
390 self.totalable_supported = true
378
391
379 def order_statement(custom_field)
392 def order_statement(custom_field)
380 # Make the database cast values into numeric
393 # Make the database cast values into numeric
381 # Postgresql will raise an error if a value can not be casted!
394 # Postgresql will raise an error if a value can not be casted!
382 # CustomValue validations should ensure that it doesn't occur
395 # CustomValue validations should ensure that it doesn't occur
383 "CAST(CASE #{join_alias custom_field}.value WHEN '' THEN '0' ELSE #{join_alias custom_field}.value END AS decimal(30,3))"
396 "CAST(CASE #{join_alias custom_field}.value WHEN '' THEN '0' ELSE #{join_alias custom_field}.value END AS decimal(30,3))"
384 end
397 end
385
398
386 # Returns totals for the given scope
399 # Returns totals for the given scope
387 def total_for_scope(custom_field, scope)
400 def total_for_scope(custom_field, scope)
388 scope.joins(:custom_values).
401 scope.joins(:custom_values).
389 where(:custom_values => {:custom_field_id => custom_field.id}).
402 where(:custom_values => {:custom_field_id => custom_field.id}).
390 where.not(:custom_values => {:value => ''}).
403 where.not(:custom_values => {:value => ''}).
391 sum("CAST(#{CustomValue.table_name}.value AS decimal(30,3))")
404 sum("CAST(#{CustomValue.table_name}.value AS decimal(30,3))")
392 end
405 end
393
406
394 def cast_total_value(custom_field, value)
407 def cast_total_value(custom_field, value)
395 cast_single_value(custom_field, value)
408 cast_single_value(custom_field, value)
396 end
409 end
397 end
410 end
398
411
399 class IntFormat < Numeric
412 class IntFormat < Numeric
400 add 'int'
413 add 'int'
401
414
402 def label
415 def label
403 "label_integer"
416 "label_integer"
404 end
417 end
405
418
406 def cast_single_value(custom_field, value, customized=nil)
419 def cast_single_value(custom_field, value, customized=nil)
407 value.to_i
420 value.to_i
408 end
421 end
409
422
410 def validate_single_value(custom_field, value, customized=nil)
423 def validate_single_value(custom_field, value, customized=nil)
411 errs = super
424 errs = super
412 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value.to_s =~ /^[+-]?\d+$/
425 errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value.to_s =~ /^[+-]?\d+$/
413 errs
426 errs
414 end
427 end
415
428
416 def query_filter_options(custom_field, query)
429 def query_filter_options(custom_field, query)
417 {:type => :integer}
430 {:type => :integer}
418 end
431 end
419
432
420 def group_statement(custom_field)
433 def group_statement(custom_field)
421 order_statement(custom_field)
434 order_statement(custom_field)
422 end
435 end
423 end
436 end
424
437
425 class FloatFormat < Numeric
438 class FloatFormat < Numeric
426 add 'float'
439 add 'float'
427
440
428 def cast_single_value(custom_field, value, customized=nil)
441 def cast_single_value(custom_field, value, customized=nil)
429 value.to_f
442 value.to_f
430 end
443 end
431
444
432 def cast_total_value(custom_field, value)
445 def cast_total_value(custom_field, value)
433 value.to_f.round(2)
446 value.to_f.round(2)
434 end
447 end
435
448
436 def validate_single_value(custom_field, value, customized=nil)
449 def validate_single_value(custom_field, value, customized=nil)
437 errs = super
450 errs = super
438 errs << ::I18n.t('activerecord.errors.messages.invalid') unless (Kernel.Float(value) rescue nil)
451 errs << ::I18n.t('activerecord.errors.messages.invalid') unless (Kernel.Float(value) rescue nil)
439 errs
452 errs
440 end
453 end
441
454
442 def query_filter_options(custom_field, query)
455 def query_filter_options(custom_field, query)
443 {:type => :float}
456 {:type => :float}
444 end
457 end
445 end
458 end
446
459
447 class DateFormat < Unbounded
460 class DateFormat < Unbounded
448 add 'date'
461 add 'date'
449 self.form_partial = 'custom_fields/formats/date'
462 self.form_partial = 'custom_fields/formats/date'
450
463
451 def cast_single_value(custom_field, value, customized=nil)
464 def cast_single_value(custom_field, value, customized=nil)
452 value.to_date rescue nil
465 value.to_date rescue nil
453 end
466 end
454
467
455 def validate_single_value(custom_field, value, customized=nil)
468 def validate_single_value(custom_field, value, customized=nil)
456 if value =~ /^\d{4}-\d{2}-\d{2}$/ && (value.to_date rescue false)
469 if value =~ /^\d{4}-\d{2}-\d{2}$/ && (value.to_date rescue false)
457 []
470 []
458 else
471 else
459 [::I18n.t('activerecord.errors.messages.not_a_date')]
472 [::I18n.t('activerecord.errors.messages.not_a_date')]
460 end
473 end
461 end
474 end
462
475
463 def edit_tag(view, tag_id, tag_name, custom_value, options={})
476 def edit_tag(view, tag_id, tag_name, custom_value, options={})
464 view.date_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :size => 10)) +
477 view.date_field_tag(tag_name, custom_value.value, options.merge(:id => tag_id, :size => 10)) +
465 view.calendar_for(tag_id)
478 view.calendar_for(tag_id)
466 end
479 end
467
480
468 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
481 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
469 view.date_field_tag(tag_name, value, options.merge(:id => tag_id, :size => 10)) +
482 view.date_field_tag(tag_name, value, options.merge(:id => tag_id, :size => 10)) +
470 view.calendar_for(tag_id) +
483 view.calendar_for(tag_id) +
471 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
484 bulk_clear_tag(view, tag_id, tag_name, custom_field, value)
472 end
485 end
473
486
474 def query_filter_options(custom_field, query)
487 def query_filter_options(custom_field, query)
475 {:type => :date}
488 {:type => :date}
476 end
489 end
477
490
478 def group_statement(custom_field)
491 def group_statement(custom_field)
479 order_statement(custom_field)
492 order_statement(custom_field)
480 end
493 end
481 end
494 end
482
495
483 class List < Base
496 class List < Base
484 self.multiple_supported = true
497 self.multiple_supported = true
485 field_attributes :edit_tag_style
498 field_attributes :edit_tag_style
486
499
487 def edit_tag(view, tag_id, tag_name, custom_value, options={})
500 def edit_tag(view, tag_id, tag_name, custom_value, options={})
488 if custom_value.custom_field.edit_tag_style == 'check_box'
501 if custom_value.custom_field.edit_tag_style == 'check_box'
489 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
502 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
490 else
503 else
491 select_edit_tag(view, tag_id, tag_name, custom_value, options)
504 select_edit_tag(view, tag_id, tag_name, custom_value, options)
492 end
505 end
493 end
506 end
494
507
495 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
508 def bulk_edit_tag(view, tag_id, tag_name, custom_field, objects, value, options={})
496 opts = []
509 opts = []
497 opts << [l(:label_no_change_option), ''] unless custom_field.multiple?
510 opts << [l(:label_no_change_option), ''] unless custom_field.multiple?
498 opts << [l(:label_none), '__none__'] unless custom_field.is_required?
511 opts << [l(:label_none), '__none__'] unless custom_field.is_required?
499 opts += possible_values_options(custom_field, objects)
512 opts += possible_values_options(custom_field, objects)
500 view.select_tag(tag_name, view.options_for_select(opts, value), options.merge(:multiple => custom_field.multiple?))
513 view.select_tag(tag_name, view.options_for_select(opts, value), options.merge(:multiple => custom_field.multiple?))
501 end
514 end
502
515
503 def query_filter_options(custom_field, query)
516 def query_filter_options(custom_field, query)
504 {:type => :list_optional, :values => query_filter_values(custom_field, query)}
517 {:type => :list_optional, :values => query_filter_values(custom_field, query)}
505 end
518 end
506
519
507 protected
520 protected
508
521
509 # Returns the values that are available in the field filter
522 # Returns the values that are available in the field filter
510 def query_filter_values(custom_field, query)
523 def query_filter_values(custom_field, query)
511 possible_values_options(custom_field, query.project)
524 possible_values_options(custom_field, query.project)
512 end
525 end
513
526
514 # Renders the edit tag as a select tag
527 # Renders the edit tag as a select tag
515 def select_edit_tag(view, tag_id, tag_name, custom_value, options={})
528 def select_edit_tag(view, tag_id, tag_name, custom_value, options={})
516 blank_option = ''.html_safe
529 blank_option = ''.html_safe
517 unless custom_value.custom_field.multiple?
530 unless custom_value.custom_field.multiple?
518 if custom_value.custom_field.is_required?
531 if custom_value.custom_field.is_required?
519 unless custom_value.custom_field.default_value.present?
532 unless custom_value.custom_field.default_value.present?
520 blank_option = view.content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
533 blank_option = view.content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
521 end
534 end
522 else
535 else
523 blank_option = view.content_tag('option', '&nbsp;'.html_safe, :value => '')
536 blank_option = view.content_tag('option', '&nbsp;'.html_safe, :value => '')
524 end
537 end
525 end
538 end
526 options_tags = blank_option + view.options_for_select(possible_custom_value_options(custom_value), custom_value.value)
539 options_tags = blank_option + view.options_for_select(possible_custom_value_options(custom_value), custom_value.value)
527 s = view.select_tag(tag_name, options_tags, options.merge(:id => tag_id, :multiple => custom_value.custom_field.multiple?))
540 s = view.select_tag(tag_name, options_tags, options.merge(:id => tag_id, :multiple => custom_value.custom_field.multiple?))
528 if custom_value.custom_field.multiple?
541 if custom_value.custom_field.multiple?
529 s << view.hidden_field_tag(tag_name, '')
542 s << view.hidden_field_tag(tag_name, '')
530 end
543 end
531 s
544 s
532 end
545 end
533
546
534 # Renders the edit tag as check box or radio tags
547 # Renders the edit tag as check box or radio tags
535 def check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
548 def check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
536 opts = []
549 opts = []
537 unless custom_value.custom_field.multiple? || custom_value.custom_field.is_required?
550 unless custom_value.custom_field.multiple? || custom_value.custom_field.is_required?
538 opts << ["(#{l(:label_none)})", '']
551 opts << ["(#{l(:label_none)})", '']
539 end
552 end
540 opts += possible_custom_value_options(custom_value)
553 opts += possible_custom_value_options(custom_value)
541 s = ''.html_safe
554 s = ''.html_safe
542 tag_method = custom_value.custom_field.multiple? ? :check_box_tag : :radio_button_tag
555 tag_method = custom_value.custom_field.multiple? ? :check_box_tag : :radio_button_tag
543 opts.each do |label, value|
556 opts.each do |label, value|
544 value ||= label
557 value ||= label
545 checked = (custom_value.value.is_a?(Array) && custom_value.value.include?(value)) || custom_value.value.to_s == value
558 checked = (custom_value.value.is_a?(Array) && custom_value.value.include?(value)) || custom_value.value.to_s == value
546 tag = view.send(tag_method, tag_name, value, checked, :id => tag_id)
559 tag = view.send(tag_method, tag_name, value, checked, :id => tag_id)
547 # set the id on the first tag only
560 # set the id on the first tag only
548 tag_id = nil
561 tag_id = nil
549 s << view.content_tag('label', tag + ' ' + label)
562 s << view.content_tag('label', tag + ' ' + label)
550 end
563 end
551 if custom_value.custom_field.multiple?
564 if custom_value.custom_field.multiple?
552 s << view.hidden_field_tag(tag_name, '')
565 s << view.hidden_field_tag(tag_name, '')
553 end
566 end
554 css = "#{options[:class]} check_box_group"
567 css = "#{options[:class]} check_box_group"
555 view.content_tag('span', s, options.merge(:class => css))
568 view.content_tag('span', s, options.merge(:class => css))
556 end
569 end
557 end
570 end
558
571
559 class ListFormat < List
572 class ListFormat < List
560 add 'list'
573 add 'list'
561 self.searchable_supported = true
574 self.searchable_supported = true
562 self.form_partial = 'custom_fields/formats/list'
575 self.form_partial = 'custom_fields/formats/list'
563
576
564 def possible_custom_value_options(custom_value)
577 def possible_custom_value_options(custom_value)
565 options = possible_values_options(custom_value.custom_field)
578 options = possible_values_options(custom_value.custom_field)
566 missing = [custom_value.value].flatten.reject(&:blank?) - options
579 missing = [custom_value.value].flatten.reject(&:blank?) - options
567 if missing.any?
580 if missing.any?
568 options += missing
581 options += missing
569 end
582 end
570 options
583 options
571 end
584 end
572
585
573 def possible_values_options(custom_field, object=nil)
586 def possible_values_options(custom_field, object=nil)
574 custom_field.possible_values
587 custom_field.possible_values
575 end
588 end
576
589
577 def validate_custom_field(custom_field)
590 def validate_custom_field(custom_field)
578 errors = []
591 errors = []
579 errors << [:possible_values, :blank] if custom_field.possible_values.blank?
592 errors << [:possible_values, :blank] if custom_field.possible_values.blank?
580 errors << [:possible_values, :invalid] unless custom_field.possible_values.is_a? Array
593 errors << [:possible_values, :invalid] unless custom_field.possible_values.is_a? Array
581 errors
594 errors
582 end
595 end
583
596
584 def validate_custom_value(custom_value)
597 def validate_custom_value(custom_value)
585 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
598 values = Array.wrap(custom_value.value).reject {|value| value.to_s == ''}
586 invalid_values = values - Array.wrap(custom_value.value_was) - custom_value.custom_field.possible_values
599 invalid_values = values - Array.wrap(custom_value.value_was) - custom_value.custom_field.possible_values
587 if invalid_values.any?
600 if invalid_values.any?
588 [::I18n.t('activerecord.errors.messages.inclusion')]
601 [::I18n.t('activerecord.errors.messages.inclusion')]
589 else
602 else
590 []
603 []
591 end
604 end
592 end
605 end
593
606
594 def group_statement(custom_field)
607 def group_statement(custom_field)
595 order_statement(custom_field)
608 order_statement(custom_field)
596 end
609 end
597 end
610 end
598
611
599 class BoolFormat < List
612 class BoolFormat < List
600 add 'bool'
613 add 'bool'
601 self.multiple_supported = false
614 self.multiple_supported = false
602 self.form_partial = 'custom_fields/formats/bool'
615 self.form_partial = 'custom_fields/formats/bool'
603
616
604 def label
617 def label
605 "label_boolean"
618 "label_boolean"
606 end
619 end
607
620
608 def cast_single_value(custom_field, value, customized=nil)
621 def cast_single_value(custom_field, value, customized=nil)
609 value == '1' ? true : false
622 value == '1' ? true : false
610 end
623 end
611
624
612 def possible_values_options(custom_field, object=nil)
625 def possible_values_options(custom_field, object=nil)
613 [[::I18n.t(:general_text_Yes), '1'], [::I18n.t(:general_text_No), '0']]
626 [[::I18n.t(:general_text_Yes), '1'], [::I18n.t(:general_text_No), '0']]
614 end
627 end
615
628
616 def group_statement(custom_field)
629 def group_statement(custom_field)
617 order_statement(custom_field)
630 order_statement(custom_field)
618 end
631 end
619
632
620 def edit_tag(view, tag_id, tag_name, custom_value, options={})
633 def edit_tag(view, tag_id, tag_name, custom_value, options={})
621 case custom_value.custom_field.edit_tag_style
634 case custom_value.custom_field.edit_tag_style
622 when 'check_box'
635 when 'check_box'
623 single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
636 single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
624 when 'radio'
637 when 'radio'
625 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
638 check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
626 else
639 else
627 select_edit_tag(view, tag_id, tag_name, custom_value, options)
640 select_edit_tag(view, tag_id, tag_name, custom_value, options)
628 end
641 end
629 end
642 end
630
643
631 # Renders the edit tag as a simple check box
644 # Renders the edit tag as a simple check box
632 def single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
645 def single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options={})
633 s = ''.html_safe
646 s = ''.html_safe
634 s << view.hidden_field_tag(tag_name, '0', :id => nil)
647 s << view.hidden_field_tag(tag_name, '0', :id => nil)
635 s << view.check_box_tag(tag_name, '1', custom_value.value.to_s == '1', :id => tag_id)
648 s << view.check_box_tag(tag_name, '1', custom_value.value.to_s == '1', :id => tag_id)
636 view.content_tag('span', s, options)
649 view.content_tag('span', s, options)
637 end
650 end
638 end
651 end
639
652
640 class RecordList < List
653 class RecordList < List
641 self.customized_class_names = %w(Issue TimeEntry Version Document Project)
654 self.customized_class_names = %w(Issue TimeEntry Version Document Project)
642
655
643 def cast_single_value(custom_field, value, customized=nil)
656 def cast_single_value(custom_field, value, customized=nil)
644 target_class.find_by_id(value.to_i) if value.present?
657 target_class.find_by_id(value.to_i) if value.present?
645 end
658 end
646
659
647 def target_class
660 def target_class
648 @target_class ||= self.class.name[/^(.*::)?(.+)Format$/, 2].constantize rescue nil
661 @target_class ||= self.class.name[/^(.*::)?(.+)Format$/, 2].constantize rescue nil
649 end
662 end
650
663
651 def reset_target_class
664 def reset_target_class
652 @target_class = nil
665 @target_class = nil
653 end
666 end
654
667
655 def possible_custom_value_options(custom_value)
668 def possible_custom_value_options(custom_value)
656 options = possible_values_options(custom_value.custom_field, custom_value.customized)
669 options = possible_values_options(custom_value.custom_field, custom_value.customized)
657 missing = [custom_value.value_was].flatten.reject(&:blank?) - options.map(&:last)
670 missing = [custom_value.value_was].flatten.reject(&:blank?) - options.map(&:last)
658 if missing.any?
671 if missing.any?
659 options += target_class.where(:id => missing.map(&:to_i)).map {|o| [o.to_s, o.id.to_s]}
672 options += target_class.where(:id => missing.map(&:to_i)).map {|o| [o.to_s, o.id.to_s]}
660 end
673 end
661 options
674 options
662 end
675 end
663
676
664 def order_statement(custom_field)
677 def order_statement(custom_field)
665 if target_class.respond_to?(:fields_for_order_statement)
678 if target_class.respond_to?(:fields_for_order_statement)
666 target_class.fields_for_order_statement(value_join_alias(custom_field))
679 target_class.fields_for_order_statement(value_join_alias(custom_field))
667 end
680 end
668 end
681 end
669
682
670 def group_statement(custom_field)
683 def group_statement(custom_field)
671 "COALESCE(#{join_alias custom_field}.value, '')"
684 "COALESCE(#{join_alias custom_field}.value, '')"
672 end
685 end
673
686
674 def join_for_order_statement(custom_field)
687 def join_for_order_statement(custom_field)
675 alias_name = join_alias(custom_field)
688 alias_name = join_alias(custom_field)
676
689
677 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
690 "LEFT OUTER JOIN #{CustomValue.table_name} #{alias_name}" +
678 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
691 " ON #{alias_name}.customized_type = '#{custom_field.class.customized_class.base_class.name}'" +
679 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
692 " AND #{alias_name}.customized_id = #{custom_field.class.customized_class.table_name}.id" +
680 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
693 " AND #{alias_name}.custom_field_id = #{custom_field.id}" +
681 " AND (#{custom_field.visibility_by_project_condition})" +
694 " AND (#{custom_field.visibility_by_project_condition})" +
682 " AND #{alias_name}.value <> ''" +
695 " AND #{alias_name}.value <> ''" +
683 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
696 " AND #{alias_name}.id = (SELECT max(#{alias_name}_2.id) FROM #{CustomValue.table_name} #{alias_name}_2" +
684 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
697 " WHERE #{alias_name}_2.customized_type = #{alias_name}.customized_type" +
685 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
698 " AND #{alias_name}_2.customized_id = #{alias_name}.customized_id" +
686 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)" +
699 " AND #{alias_name}_2.custom_field_id = #{alias_name}.custom_field_id)" +
687 " LEFT OUTER JOIN #{target_class.table_name} #{value_join_alias custom_field}" +
700 " LEFT OUTER JOIN #{target_class.table_name} #{value_join_alias custom_field}" +
688 " ON CAST(CASE #{alias_name}.value WHEN '' THEN '0' ELSE #{alias_name}.value END AS decimal(30,0)) = #{value_join_alias custom_field}.id"
701 " ON CAST(CASE #{alias_name}.value WHEN '' THEN '0' ELSE #{alias_name}.value END AS decimal(30,0)) = #{value_join_alias custom_field}.id"
689 end
702 end
690
703
691 def value_join_alias(custom_field)
704 def value_join_alias(custom_field)
692 join_alias(custom_field) + "_" + custom_field.field_format
705 join_alias(custom_field) + "_" + custom_field.field_format
693 end
706 end
694 protected :value_join_alias
707 protected :value_join_alias
695 end
708 end
696
709
697 class EnumerationFormat < RecordList
710 class EnumerationFormat < RecordList
698 add 'enumeration'
711 add 'enumeration'
699 self.form_partial = 'custom_fields/formats/enumeration'
712 self.form_partial = 'custom_fields/formats/enumeration'
700
713
701 def label
714 def label
702 "label_field_format_enumeration"
715 "label_field_format_enumeration"
703 end
716 end
704
717
705 def target_class
718 def target_class
706 @target_class ||= CustomFieldEnumeration
719 @target_class ||= CustomFieldEnumeration
707 end
720 end
708
721
709 def possible_values_options(custom_field, object=nil)
722 def possible_values_options(custom_field, object=nil)
710 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
723 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
711 end
724 end
712
725
713 def possible_values_records(custom_field, object=nil)
726 def possible_values_records(custom_field, object=nil)
714 custom_field.enumerations.active
727 custom_field.enumerations.active
715 end
728 end
716
729
717 def value_from_keyword(custom_field, keyword, object)
730 def value_from_keyword(custom_field, keyword, object)
718 value = custom_field.enumerations.where("LOWER(name) LIKE LOWER(?)", keyword).first
731 value = custom_field.enumerations.where("LOWER(name) LIKE LOWER(?)", keyword).first
719 value ? value.id : nil
732 value ? value.id : nil
720 end
733 end
721 end
734 end
722
735
723 class UserFormat < RecordList
736 class UserFormat < RecordList
724 add 'user'
737 add 'user'
725 self.form_partial = 'custom_fields/formats/user'
738 self.form_partial = 'custom_fields/formats/user'
726 field_attributes :user_role
739 field_attributes :user_role
727
740
728 def possible_values_options(custom_field, object=nil)
741 def possible_values_options(custom_field, object=nil)
729 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
742 possible_values_records(custom_field, object).map {|u| [u.name, u.id.to_s]}
730 end
743 end
731
744
732 def possible_values_records(custom_field, object=nil)
745 def possible_values_records(custom_field, object=nil)
733 if object.is_a?(Array)
746 if object.is_a?(Array)
734 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
747 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
735 projects.map {|project| possible_values_records(custom_field, project)}.reduce(:&) || []
748 projects.map {|project| possible_values_records(custom_field, project)}.reduce(:&) || []
736 elsif object.respond_to?(:project) && object.project
749 elsif object.respond_to?(:project) && object.project
737 scope = object.project.users
750 scope = object.project.users
738 if custom_field.user_role.is_a?(Array)
751 if custom_field.user_role.is_a?(Array)
739 role_ids = custom_field.user_role.map(&:to_s).reject(&:blank?).map(&:to_i)
752 role_ids = custom_field.user_role.map(&:to_s).reject(&:blank?).map(&:to_i)
740 if role_ids.any?
753 if role_ids.any?
741 scope = scope.where("#{Member.table_name}.id IN (SELECT DISTINCT member_id FROM #{MemberRole.table_name} WHERE role_id IN (?))", role_ids)
754 scope = scope.where("#{Member.table_name}.id IN (SELECT DISTINCT member_id FROM #{MemberRole.table_name} WHERE role_id IN (?))", role_ids)
742 end
755 end
743 end
756 end
744 scope.sorted
757 scope.sorted
745 else
758 else
746 []
759 []
747 end
760 end
748 end
761 end
749
762
750 def value_from_keyword(custom_field, keyword, object)
763 def value_from_keyword(custom_field, keyword, object)
751 users = possible_values_records(custom_field, object).to_a
764 users = possible_values_records(custom_field, object).to_a
752 user = Principal.detect_by_keyword(users, keyword)
765 user = Principal.detect_by_keyword(users, keyword)
753 user ? user.id : nil
766 user ? user.id : nil
754 end
767 end
755
768
756 def before_custom_field_save(custom_field)
769 def before_custom_field_save(custom_field)
757 super
770 super
758 if custom_field.user_role.is_a?(Array)
771 if custom_field.user_role.is_a?(Array)
759 custom_field.user_role.map!(&:to_s).reject!(&:blank?)
772 custom_field.user_role.map!(&:to_s).reject!(&:blank?)
760 end
773 end
761 end
774 end
762 end
775 end
763
776
764 class VersionFormat < RecordList
777 class VersionFormat < RecordList
765 add 'version'
778 add 'version'
766 self.form_partial = 'custom_fields/formats/version'
779 self.form_partial = 'custom_fields/formats/version'
767 field_attributes :version_status
780 field_attributes :version_status
768
781
769 def possible_values_options(custom_field, object=nil)
782 def possible_values_options(custom_field, object=nil)
770 versions_options(custom_field, object)
783 versions_options(custom_field, object)
771 end
784 end
772
785
773 def before_custom_field_save(custom_field)
786 def before_custom_field_save(custom_field)
774 super
787 super
775 if custom_field.version_status.is_a?(Array)
788 if custom_field.version_status.is_a?(Array)
776 custom_field.version_status.map!(&:to_s).reject!(&:blank?)
789 custom_field.version_status.map!(&:to_s).reject!(&:blank?)
777 end
790 end
778 end
791 end
779
792
780 protected
793 protected
781
794
782 def query_filter_values(custom_field, query)
795 def query_filter_values(custom_field, query)
783 versions_options(custom_field, query.project, true)
796 versions_options(custom_field, query.project, true)
784 end
797 end
785
798
786 def versions_options(custom_field, object, all_statuses=false)
799 def versions_options(custom_field, object, all_statuses=false)
787 if object.is_a?(Array)
800 if object.is_a?(Array)
788 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
801 projects = object.map {|o| o.respond_to?(:project) ? o.project : nil}.compact.uniq
789 projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
802 projects.map {|project| possible_values_options(custom_field, project)}.reduce(:&) || []
790 elsif object.respond_to?(:project) && object.project
803 elsif object.respond_to?(:project) && object.project
791 scope = object.project.shared_versions
804 scope = object.project.shared_versions
792 if !all_statuses && custom_field.version_status.is_a?(Array)
805 if !all_statuses && custom_field.version_status.is_a?(Array)
793 statuses = custom_field.version_status.map(&:to_s).reject(&:blank?)
806 statuses = custom_field.version_status.map(&:to_s).reject(&:blank?)
794 if statuses.any?
807 if statuses.any?
795 scope = scope.where(:status => statuses.map(&:to_s))
808 scope = scope.where(:status => statuses.map(&:to_s))
796 end
809 end
797 end
810 end
798 scope.sort.collect {|u| [u.to_s, u.id.to_s]}
811 scope.sort.collect {|u| [u.to_s, u.id.to_s]}
799 else
812 else
800 []
813 []
801 end
814 end
802 end
815 end
803 end
816 end
804 end
817 end
805 end
818 end
@@ -1,138 +1,147
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'cgi'
18 require 'cgi'
19
19
20 module Redmine
20 module Redmine
21 module WikiFormatting
21 module WikiFormatting
22 module Markdown
22 module Markdown
23 class HTML < Redcarpet::Render::HTML
23 class HTML < Redcarpet::Render::HTML
24 include ActionView::Helpers::TagHelper
24 include ActionView::Helpers::TagHelper
25 include Redmine::Helpers::URL
25
26
26 def link(link, title, content)
27 def link(link, title, content)
28 return nil unless uri_with_safe_scheme?(link)
29
27 css = nil
30 css = nil
28 unless link && link.starts_with?('/')
31 unless link && link.starts_with?('/')
29 css = 'external'
32 css = 'external'
30 end
33 end
31 content_tag('a', content.to_s.html_safe, :href => link, :title => title, :class => css)
34 content_tag('a', content.to_s.html_safe, :href => link, :title => title, :class => css)
32 end
35 end
33
36
34 def block_code(code, language)
37 def block_code(code, language)
35 if language.present?
38 if language.present?
36 "<pre><code class=\"#{CGI.escapeHTML language} syntaxhl\">" +
39 "<pre><code class=\"#{CGI.escapeHTML language} syntaxhl\">" +
37 Redmine::SyntaxHighlighting.highlight_by_language(code, language) +
40 Redmine::SyntaxHighlighting.highlight_by_language(code, language) +
38 "</code></pre>"
41 "</code></pre>"
39 else
42 else
40 "<pre>" + CGI.escapeHTML(code) + "</pre>"
43 "<pre>" + CGI.escapeHTML(code) + "</pre>"
41 end
44 end
42 end
45 end
46
47 def image(link, title, alt_text)
48 return unless uri_with_safe_scheme?(link)
49
50 tag('img', :src => link, :alt => alt_text || "", :title => title)
51 end
43 end
52 end
44
53
45 class Formatter
54 class Formatter
46 def initialize(text)
55 def initialize(text)
47 @text = text
56 @text = text
48 end
57 end
49
58
50 def to_html(*args)
59 def to_html(*args)
51 html = formatter.render(@text)
60 html = formatter.render(@text)
52 # restore wiki links eg. [[Foo]]
61 # restore wiki links eg. [[Foo]]
53 html.gsub!(%r{\[<a href="(.*?)">(.*?)</a>\]}) do
62 html.gsub!(%r{\[<a href="(.*?)">(.*?)</a>\]}) do
54 "[[#{$2}]]"
63 "[[#{$2}]]"
55 end
64 end
56 # restore Redmine links with double-quotes, eg. version:"1.0"
65 # restore Redmine links with double-quotes, eg. version:"1.0"
57 html.gsub!(/(\w):&quot;(.+?)&quot;/) do
66 html.gsub!(/(\w):&quot;(.+?)&quot;/) do
58 "#{$1}:\"#{$2}\""
67 "#{$1}:\"#{$2}\""
59 end
68 end
60 html
69 html
61 end
70 end
62
71
63 def get_section(index)
72 def get_section(index)
64 section = extract_sections(index)[1]
73 section = extract_sections(index)[1]
65 hash = Digest::MD5.hexdigest(section)
74 hash = Digest::MD5.hexdigest(section)
66 return section, hash
75 return section, hash
67 end
76 end
68
77
69 def update_section(index, update, hash=nil)
78 def update_section(index, update, hash=nil)
70 t = extract_sections(index)
79 t = extract_sections(index)
71 if hash.present? && hash != Digest::MD5.hexdigest(t[1])
80 if hash.present? && hash != Digest::MD5.hexdigest(t[1])
72 raise Redmine::WikiFormatting::StaleSectionError
81 raise Redmine::WikiFormatting::StaleSectionError
73 end
82 end
74 t[1] = update unless t[1].blank?
83 t[1] = update unless t[1].blank?
75 t.reject(&:blank?).join "\n\n"
84 t.reject(&:blank?).join "\n\n"
76 end
85 end
77
86
78 def extract_sections(index)
87 def extract_sections(index)
79 sections = ['', '', '']
88 sections = ['', '', '']
80 offset = 0
89 offset = 0
81 i = 0
90 i = 0
82 l = 1
91 l = 1
83 inside_pre = false
92 inside_pre = false
84 @text.split(/(^(?:.+\r?\n\r?(?:\=+|\-+)|#+.+|~~~.*)\s*$)/).each do |part|
93 @text.split(/(^(?:.+\r?\n\r?(?:\=+|\-+)|#+.+|~~~.*)\s*$)/).each do |part|
85 level = nil
94 level = nil
86 if part =~ /\A~{3,}(\S+)?\s*$/
95 if part =~ /\A~{3,}(\S+)?\s*$/
87 if $1
96 if $1
88 if !inside_pre
97 if !inside_pre
89 inside_pre = true
98 inside_pre = true
90 end
99 end
91 else
100 else
92 inside_pre = !inside_pre
101 inside_pre = !inside_pre
93 end
102 end
94 elsif inside_pre
103 elsif inside_pre
95 # nop
104 # nop
96 elsif part =~ /\A(#+).+/
105 elsif part =~ /\A(#+).+/
97 level = $1.size
106 level = $1.size
98 elsif part =~ /\A.+\r?\n\r?(\=+|\-+)\s*$/
107 elsif part =~ /\A.+\r?\n\r?(\=+|\-+)\s*$/
99 level = $1.include?('=') ? 1 : 2
108 level = $1.include?('=') ? 1 : 2
100 end
109 end
101 if level
110 if level
102 i += 1
111 i += 1
103 if offset == 0 && i == index
112 if offset == 0 && i == index
104 # entering the requested section
113 # entering the requested section
105 offset = 1
114 offset = 1
106 l = level
115 l = level
107 elsif offset == 1 && i > index && level <= l
116 elsif offset == 1 && i > index && level <= l
108 # leaving the requested section
117 # leaving the requested section
109 offset = 2
118 offset = 2
110 end
119 end
111 end
120 end
112 sections[offset] << part
121 sections[offset] << part
113 end
122 end
114 sections.map(&:strip)
123 sections.map(&:strip)
115 end
124 end
116
125
117 private
126 private
118
127
119 def formatter
128 def formatter
120 @@formatter ||= Redcarpet::Markdown.new(
129 @@formatter ||= Redcarpet::Markdown.new(
121 Redmine::WikiFormatting::Markdown::HTML.new(
130 Redmine::WikiFormatting::Markdown::HTML.new(
122 :filter_html => true,
131 :filter_html => true,
123 :hard_wrap => true
132 :hard_wrap => true
124 ),
133 ),
125 :autolink => true,
134 :autolink => true,
126 :fenced_code_blocks => true,
135 :fenced_code_blocks => true,
127 :space_after_headers => true,
136 :space_after_headers => true,
128 :tables => true,
137 :tables => true,
129 :strikethrough => true,
138 :strikethrough => true,
130 :superscript => true,
139 :superscript => true,
131 :no_intra_emphasis => true,
140 :no_intra_emphasis => true,
132 :footnotes => true
141 :footnotes => true
133 )
142 )
134 end
143 end
135 end
144 end
136 end
145 end
137 end
146 end
138 end
147 end
@@ -1,1541 +1,1541
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require File.expand_path('../../../test_helper', __FILE__)
20 require File.expand_path('../../../test_helper', __FILE__)
21
21
22 class ApplicationHelperTest < ActionView::TestCase
22 class ApplicationHelperTest < ActionView::TestCase
23 include Redmine::I18n
23 include Redmine::I18n
24 include ERB::Util
24 include ERB::Util
25 include Rails.application.routes.url_helpers
25 include Rails.application.routes.url_helpers
26
26
27 fixtures :projects, :roles, :enabled_modules, :users,
27 fixtures :projects, :roles, :enabled_modules, :users,
28 :email_addresses,
28 :email_addresses,
29 :repositories, :changesets,
29 :repositories, :changesets,
30 :projects_trackers,
30 :projects_trackers,
31 :trackers, :issue_statuses, :issues, :versions, :documents,
31 :trackers, :issue_statuses, :issues, :versions, :documents,
32 :wikis, :wiki_pages, :wiki_contents,
32 :wikis, :wiki_pages, :wiki_contents,
33 :boards, :messages, :news,
33 :boards, :messages, :news,
34 :attachments, :enumerations
34 :attachments, :enumerations
35
35
36 def setup
36 def setup
37 super
37 super
38 set_tmp_attachments_directory
38 set_tmp_attachments_directory
39 @russian_test = "\xd1\x82\xd0\xb5\xd1\x81\xd1\x82".force_encoding('UTF-8')
39 @russian_test = "\xd1\x82\xd0\xb5\xd1\x81\xd1\x82".force_encoding('UTF-8')
40 end
40 end
41
41
42 test "#link_to_if_authorized for authorized user should allow using the :controller and :action for the target link" do
42 test "#link_to_if_authorized for authorized user should allow using the :controller and :action for the target link" do
43 User.current = User.find_by_login('admin')
43 User.current = User.find_by_login('admin')
44
44
45 @project = Issue.first.project # Used by helper
45 @project = Issue.first.project # Used by helper
46 response = link_to_if_authorized('By controller/actionr',
46 response = link_to_if_authorized('By controller/actionr',
47 {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
47 {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
48 assert_match /href/, response
48 assert_match /href/, response
49 end
49 end
50
50
51 test "#link_to_if_authorized for unauthorized user should display nothing if user isn't authorized" do
51 test "#link_to_if_authorized for unauthorized user should display nothing if user isn't authorized" do
52 User.current = User.find_by_login('dlopper')
52 User.current = User.find_by_login('dlopper')
53 @project = Project.find('private-child')
53 @project = Project.find('private-child')
54 issue = @project.issues.first
54 issue = @project.issues.first
55 assert !issue.visible?
55 assert !issue.visible?
56
56
57 response = link_to_if_authorized('Never displayed',
57 response = link_to_if_authorized('Never displayed',
58 {:controller => 'issues', :action => 'show', :id => issue})
58 {:controller => 'issues', :action => 'show', :id => issue})
59 assert_nil response
59 assert_nil response
60 end
60 end
61
61
62 def test_auto_links
62 def test_auto_links
63 to_test = {
63 to_test = {
64 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
64 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
65 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
65 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
66 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
66 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
67 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
67 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
68 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
68 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
69 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
69 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
70 'http://foo.bar/foo.bar#foo.bar.' => '<a class="external" href="http://foo.bar/foo.bar#foo.bar">http://foo.bar/foo.bar#foo.bar</a>.',
70 'http://foo.bar/foo.bar#foo.bar.' => '<a class="external" href="http://foo.bar/foo.bar#foo.bar">http://foo.bar/foo.bar#foo.bar</a>.',
71 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
71 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
72 '(see inline link : http://www.foo.bar/Test_(foobar))' => '(see inline link : <a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>)',
72 '(see inline link : http://www.foo.bar/Test_(foobar))' => '(see inline link : <a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>)',
73 '(see inline link : http://www.foo.bar/Test)' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>)',
73 '(see inline link : http://www.foo.bar/Test)' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>)',
74 '(see inline link : http://www.foo.bar/Test).' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>).',
74 '(see inline link : http://www.foo.bar/Test).' => '(see inline link : <a class="external" href="http://www.foo.bar/Test">http://www.foo.bar/Test</a>).',
75 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
75 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
76 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
76 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
77 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
77 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
78 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
78 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
79 'http://foo.bar/page?p=1&t=z&s=' => '<a class="external" href="http://foo.bar/page?p=1&#38;t=z&#38;s=">http://foo.bar/page?p=1&#38;t=z&#38;s=</a>',
79 'http://foo.bar/page?p=1&t=z&s=' => '<a class="external" href="http://foo.bar/page?p=1&#38;t=z&#38;s=">http://foo.bar/page?p=1&#38;t=z&#38;s=</a>',
80 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
80 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
81 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
81 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
82 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
82 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
83 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
83 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
84 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
84 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
85 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
85 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
86 # two exclamation marks
86 # two exclamation marks
87 'http://example.net/path!602815048C7B5C20!302.html' => '<a class="external" href="http://example.net/path!602815048C7B5C20!302.html">http://example.net/path!602815048C7B5C20!302.html</a>',
87 'http://example.net/path!602815048C7B5C20!302.html' => '<a class="external" href="http://example.net/path!602815048C7B5C20!302.html">http://example.net/path!602815048C7B5C20!302.html</a>',
88 # escaping
88 # escaping
89 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo&quot;bar</a>',
89 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo&quot;bar</a>',
90 # wrap in angle brackets
90 # wrap in angle brackets
91 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;',
91 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;',
92 # invalid urls
92 # invalid urls
93 'http://' => 'http://',
93 'http://' => 'http://',
94 'www.' => 'www.',
94 'www.' => 'www.',
95 'test-www.bar.com' => 'test-www.bar.com',
95 'test-www.bar.com' => 'test-www.bar.com',
96 }
96 }
97 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
97 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
98 end
98 end
99
99
100 def test_auto_links_with_non_ascii_characters
100 def test_auto_links_with_non_ascii_characters
101 to_test = {
101 to_test = {
102 "http://foo.bar/#{@russian_test}" =>
102 "http://foo.bar/#{@russian_test}" =>
103 %|<a class="external" href="http://foo.bar/#{@russian_test}">http://foo.bar/#{@russian_test}</a>|
103 %|<a class="external" href="http://foo.bar/#{@russian_test}">http://foo.bar/#{@russian_test}</a>|
104 }
104 }
105 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
105 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
106 end
106 end
107
107
108 def test_auto_mailto
108 def test_auto_mailto
109 to_test = {
109 to_test = {
110 'test@foo.bar' => '<a class="email" href="mailto:test@foo.bar">test@foo.bar</a>',
110 'test@foo.bar' => '<a class="email" href="mailto:test@foo.bar">test@foo.bar</a>',
111 'test@www.foo.bar' => '<a class="email" href="mailto:test@www.foo.bar">test@www.foo.bar</a>',
111 'test@www.foo.bar' => '<a class="email" href="mailto:test@www.foo.bar">test@www.foo.bar</a>',
112 }
112 }
113 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
113 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
114 end
114 end
115
115
116 def test_inline_images
116 def test_inline_images
117 to_test = {
117 to_test = {
118 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
118 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
119 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
119 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
120 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
120 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
121 'with style !{width:100px;height:100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" style="width:100px;height:100px;" alt="" />',
121 'with style !{width:100px;height:100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" style="width:100px;height:100px;" alt="" />',
122 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a title" alt="This is a title" />',
122 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a title" alt="This is a title" />',
123 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a double-quoted &quot;title&quot;" alt="This is a double-quoted &quot;title&quot;" />',
123 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a double-quoted &quot;title&quot;" alt="This is a double-quoted &quot;title&quot;" />',
124 }
124 }
125 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
125 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
126 end
126 end
127
127
128 def test_inline_images_inside_tags
128 def test_inline_images_inside_tags
129 raw = <<-RAW
129 raw = <<-RAW
130 h1. !foo.png! Heading
130 h1. !foo.png! Heading
131
131
132 Centered image:
132 Centered image:
133
133
134 p=. !bar.gif!
134 p=. !bar.gif!
135 RAW
135 RAW
136
136
137 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
137 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
138 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
138 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
139 end
139 end
140
140
141 def test_attached_images
141 def test_attached_images
142 to_test = {
142 to_test = {
143 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" />',
143 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" />',
144 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" />',
144 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" />',
145 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
145 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
146 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
146 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
147 # link image
147 # link image
148 '!logo.gif!:http://foo.bar/' => '<a href="http://foo.bar/"><img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" /></a>',
148 '!logo.gif!:http://foo.bar/' => '<a href="http://foo.bar/"><img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" /></a>',
149 }
149 }
150 attachments = Attachment.all
150 attachments = Attachment.all
151 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
151 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
152 end
152 end
153
153
154 def test_attached_images_with_textile_and_non_ascii_filename
154 def test_attached_images_with_textile_and_non_ascii_filename
155 attachment = Attachment.generate!(:filename => 'cafΓ©.jpg')
155 attachment = Attachment.generate!(:filename => 'cafΓ©.jpg')
156 with_settings :text_formatting => 'textile' do
156 with_settings :text_formatting => 'textile' do
157 assert_include %(<img src="/attachments/download/#{attachment.id}/caf%C3%A9.jpg" alt="" />),
157 assert_include %(<img src="/attachments/download/#{attachment.id}/caf%C3%A9.jpg" alt="" />),
158 textilizable("!cafΓ©.jpg!)", :attachments => [attachment])
158 textilizable("!cafΓ©.jpg!)", :attachments => [attachment])
159 end
159 end
160 end
160 end
161
161
162 def test_attached_images_with_markdown_and_non_ascii_filename
162 def test_attached_images_with_markdown_and_non_ascii_filename
163 skip unless Object.const_defined?(:Redcarpet)
163 skip unless Object.const_defined?(:Redcarpet)
164
164
165 attachment = Attachment.generate!(:filename => 'cafΓ©.jpg')
165 attachment = Attachment.generate!(:filename => 'cafΓ©.jpg')
166 with_settings :text_formatting => 'markdown' do
166 with_settings :text_formatting => 'markdown' do
167 assert_include %(<img src="/attachments/download/#{attachment.id}/caf%C3%A9.jpg" alt="">),
167 assert_include %(<img src="/attachments/download/#{attachment.id}/caf%C3%A9.jpg" alt="" />),
168 textilizable("![](cafΓ©.jpg)", :attachments => [attachment])
168 textilizable("![](cafΓ©.jpg)", :attachments => [attachment])
169 end
169 end
170 end
170 end
171
171
172 def test_attached_images_filename_extension
172 def test_attached_images_filename_extension
173 set_tmp_attachments_directory
173 set_tmp_attachments_directory
174 a1 = Attachment.new(
174 a1 = Attachment.new(
175 :container => Issue.find(1),
175 :container => Issue.find(1),
176 :file => mock_file_with_options({:original_filename => "testtest.JPG"}),
176 :file => mock_file_with_options({:original_filename => "testtest.JPG"}),
177 :author => User.find(1))
177 :author => User.find(1))
178 assert a1.save
178 assert a1.save
179 assert_equal "testtest.JPG", a1.filename
179 assert_equal "testtest.JPG", a1.filename
180 assert_equal "image/jpeg", a1.content_type
180 assert_equal "image/jpeg", a1.content_type
181 assert a1.image?
181 assert a1.image?
182
182
183 a2 = Attachment.new(
183 a2 = Attachment.new(
184 :container => Issue.find(1),
184 :container => Issue.find(1),
185 :file => mock_file_with_options({:original_filename => "testtest.jpeg"}),
185 :file => mock_file_with_options({:original_filename => "testtest.jpeg"}),
186 :author => User.find(1))
186 :author => User.find(1))
187 assert a2.save
187 assert a2.save
188 assert_equal "testtest.jpeg", a2.filename
188 assert_equal "testtest.jpeg", a2.filename
189 assert_equal "image/jpeg", a2.content_type
189 assert_equal "image/jpeg", a2.content_type
190 assert a2.image?
190 assert a2.image?
191
191
192 a3 = Attachment.new(
192 a3 = Attachment.new(
193 :container => Issue.find(1),
193 :container => Issue.find(1),
194 :file => mock_file_with_options({:original_filename => "testtest.JPE"}),
194 :file => mock_file_with_options({:original_filename => "testtest.JPE"}),
195 :author => User.find(1))
195 :author => User.find(1))
196 assert a3.save
196 assert a3.save
197 assert_equal "testtest.JPE", a3.filename
197 assert_equal "testtest.JPE", a3.filename
198 assert_equal "image/jpeg", a3.content_type
198 assert_equal "image/jpeg", a3.content_type
199 assert a3.image?
199 assert a3.image?
200
200
201 a4 = Attachment.new(
201 a4 = Attachment.new(
202 :container => Issue.find(1),
202 :container => Issue.find(1),
203 :file => mock_file_with_options({:original_filename => "Testtest.BMP"}),
203 :file => mock_file_with_options({:original_filename => "Testtest.BMP"}),
204 :author => User.find(1))
204 :author => User.find(1))
205 assert a4.save
205 assert a4.save
206 assert_equal "Testtest.BMP", a4.filename
206 assert_equal "Testtest.BMP", a4.filename
207 assert_equal "image/x-ms-bmp", a4.content_type
207 assert_equal "image/x-ms-bmp", a4.content_type
208 assert a4.image?
208 assert a4.image?
209
209
210 to_test = {
210 to_test = {
211 'Inline image: !testtest.jpg!' =>
211 'Inline image: !testtest.jpg!' =>
212 'Inline image: <img src="/attachments/download/' + a1.id.to_s + '/testtest.JPG" alt="" />',
212 'Inline image: <img src="/attachments/download/' + a1.id.to_s + '/testtest.JPG" alt="" />',
213 'Inline image: !testtest.jpeg!' =>
213 'Inline image: !testtest.jpeg!' =>
214 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testtest.jpeg" alt="" />',
214 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testtest.jpeg" alt="" />',
215 'Inline image: !testtest.jpe!' =>
215 'Inline image: !testtest.jpe!' =>
216 'Inline image: <img src="/attachments/download/' + a3.id.to_s + '/testtest.JPE" alt="" />',
216 'Inline image: <img src="/attachments/download/' + a3.id.to_s + '/testtest.JPE" alt="" />',
217 'Inline image: !testtest.bmp!' =>
217 'Inline image: !testtest.bmp!' =>
218 'Inline image: <img src="/attachments/download/' + a4.id.to_s + '/Testtest.BMP" alt="" />',
218 'Inline image: <img src="/attachments/download/' + a4.id.to_s + '/Testtest.BMP" alt="" />',
219 }
219 }
220
220
221 attachments = [a1, a2, a3, a4]
221 attachments = [a1, a2, a3, a4]
222 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
222 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
223 end
223 end
224
224
225 def test_attached_images_should_read_later
225 def test_attached_images_should_read_later
226 set_fixtures_attachments_directory
226 set_fixtures_attachments_directory
227 a1 = Attachment.find(16)
227 a1 = Attachment.find(16)
228 assert_equal "testfile.png", a1.filename
228 assert_equal "testfile.png", a1.filename
229 assert a1.readable?
229 assert a1.readable?
230 assert (! a1.visible?(User.anonymous))
230 assert (! a1.visible?(User.anonymous))
231 assert a1.visible?(User.find(2))
231 assert a1.visible?(User.find(2))
232 a2 = Attachment.find(17)
232 a2 = Attachment.find(17)
233 assert_equal "testfile.PNG", a2.filename
233 assert_equal "testfile.PNG", a2.filename
234 assert a2.readable?
234 assert a2.readable?
235 assert (! a2.visible?(User.anonymous))
235 assert (! a2.visible?(User.anonymous))
236 assert a2.visible?(User.find(2))
236 assert a2.visible?(User.find(2))
237 assert a1.created_on < a2.created_on
237 assert a1.created_on < a2.created_on
238
238
239 to_test = {
239 to_test = {
240 'Inline image: !testfile.png!' =>
240 'Inline image: !testfile.png!' =>
241 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" />',
241 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" />',
242 'Inline image: !Testfile.PNG!' =>
242 'Inline image: !Testfile.PNG!' =>
243 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" />',
243 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" />',
244 }
244 }
245 attachments = [a1, a2]
245 attachments = [a1, a2]
246 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
246 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
247 set_tmp_attachments_directory
247 set_tmp_attachments_directory
248 end
248 end
249
249
250 def test_textile_external_links
250 def test_textile_external_links
251 to_test = {
251 to_test = {
252 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
252 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
253 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
253 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
254 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
254 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
255 '"link (Link title with "double-quotes")":http://foo.bar' => '<a href="http://foo.bar" title="Link title with &quot;double-quotes&quot;" class="external">link</a>',
255 '"link (Link title with "double-quotes")":http://foo.bar' => '<a href="http://foo.bar" title="Link title with &quot;double-quotes&quot;" class="external">link</a>',
256 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
256 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
257 # no multiline link text
257 # no multiline link text
258 "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line<br />and another on a second line\":test",
258 "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line<br />and another on a second line\":test",
259 # mailto link
259 # mailto link
260 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
260 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
261 # two exclamation marks
261 # two exclamation marks
262 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
262 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
263 # escaping
263 # escaping
264 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
264 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
265 }
265 }
266 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
266 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
267 end
267 end
268
268
269 def test_textile_external_links_with_non_ascii_characters
269 def test_textile_external_links_with_non_ascii_characters
270 to_test = {
270 to_test = {
271 %|This is a "link":http://foo.bar/#{@russian_test}| =>
271 %|This is a "link":http://foo.bar/#{@russian_test}| =>
272 %|This is a <a href="http://foo.bar/#{@russian_test}" class="external">link</a>|
272 %|This is a <a href="http://foo.bar/#{@russian_test}" class="external">link</a>|
273 }
273 }
274 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
274 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
275 end
275 end
276
276
277 def test_redmine_links
277 def test_redmine_links
278 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
278 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
279 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
279 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
280 note_link = link_to('#3-14', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
280 note_link = link_to('#3-14', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
281 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
281 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
282 note_link2 = link_to('#3#note-14', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
282 note_link2 = link_to('#3#note-14', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
283 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
283 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
284
284
285 revision_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
285 revision_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
286 :class => 'changeset', :title => 'My very first commit do not escaping #<>&')
286 :class => 'changeset', :title => 'My very first commit do not escaping #<>&')
287 revision_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
287 revision_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
288 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
288 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
289
289
290 changeset_link2 = link_to('691322a8eb01e11fd7',
290 changeset_link2 = link_to('691322a8eb01e11fd7',
291 {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
291 {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
292 :class => 'changeset', :title => 'My very first commit do not escaping #<>&')
292 :class => 'changeset', :title => 'My very first commit do not escaping #<>&')
293
293
294 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
294 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
295 :class => 'document')
295 :class => 'document')
296
296
297 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
297 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
298 :class => 'version')
298 :class => 'version')
299
299
300 board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'}
300 board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'}
301
301
302 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
302 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
303
303
304 news_url = {:controller => 'news', :action => 'show', :id => 1}
304 news_url = {:controller => 'news', :action => 'show', :id => 1}
305
305
306 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
306 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
307
307
308 source_url = '/projects/ecookbook/repository/entry/some/file'
308 source_url = '/projects/ecookbook/repository/entry/some/file'
309 source_url_with_rev = '/projects/ecookbook/repository/revisions/52/entry/some/file'
309 source_url_with_rev = '/projects/ecookbook/repository/revisions/52/entry/some/file'
310 source_url_with_ext = '/projects/ecookbook/repository/entry/some/file.ext'
310 source_url_with_ext = '/projects/ecookbook/repository/entry/some/file.ext'
311 source_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/entry/some/file.ext'
311 source_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/entry/some/file.ext'
312 source_url_with_branch = '/projects/ecookbook/repository/revisions/branch/entry/some/file'
312 source_url_with_branch = '/projects/ecookbook/repository/revisions/branch/entry/some/file'
313
313
314 export_url = '/projects/ecookbook/repository/raw/some/file'
314 export_url = '/projects/ecookbook/repository/raw/some/file'
315 export_url_with_rev = '/projects/ecookbook/repository/revisions/52/raw/some/file'
315 export_url_with_rev = '/projects/ecookbook/repository/revisions/52/raw/some/file'
316 export_url_with_ext = '/projects/ecookbook/repository/raw/some/file.ext'
316 export_url_with_ext = '/projects/ecookbook/repository/raw/some/file.ext'
317 export_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/raw/some/file.ext'
317 export_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/raw/some/file.ext'
318 export_url_with_branch = '/projects/ecookbook/repository/revisions/branch/raw/some/file'
318 export_url_with_branch = '/projects/ecookbook/repository/revisions/branch/raw/some/file'
319
319
320 to_test = {
320 to_test = {
321 # tickets
321 # tickets
322 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
322 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
323 # ticket notes
323 # ticket notes
324 '#3-14' => note_link,
324 '#3-14' => note_link,
325 '#3#note-14' => note_link2,
325 '#3#note-14' => note_link2,
326 # should not ignore leading zero
326 # should not ignore leading zero
327 '#03' => '#03',
327 '#03' => '#03',
328 # changesets
328 # changesets
329 'r1' => revision_link,
329 'r1' => revision_link,
330 'r1.' => "#{revision_link}.",
330 'r1.' => "#{revision_link}.",
331 'r1, r2' => "#{revision_link}, #{revision_link2}",
331 'r1, r2' => "#{revision_link}, #{revision_link2}",
332 'r1,r2' => "#{revision_link},#{revision_link2}",
332 'r1,r2' => "#{revision_link},#{revision_link2}",
333 'commit:691322a8eb01e11fd7' => changeset_link2,
333 'commit:691322a8eb01e11fd7' => changeset_link2,
334 # documents
334 # documents
335 'document#1' => document_link,
335 'document#1' => document_link,
336 'document:"Test document"' => document_link,
336 'document:"Test document"' => document_link,
337 # versions
337 # versions
338 'version#2' => version_link,
338 'version#2' => version_link,
339 'version:1.0' => version_link,
339 'version:1.0' => version_link,
340 'version:"1.0"' => version_link,
340 'version:"1.0"' => version_link,
341 # source
341 # source
342 'source:some/file' => link_to('source:some/file', source_url, :class => 'source'),
342 'source:some/file' => link_to('source:some/file', source_url, :class => 'source'),
343 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
343 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
344 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
344 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
345 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
345 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
346 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
346 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
347 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
347 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
348 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
348 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
349 'source:/some/file@52' => link_to('source:/some/file@52', source_url_with_rev, :class => 'source'),
349 'source:/some/file@52' => link_to('source:/some/file@52', source_url_with_rev, :class => 'source'),
350 'source:/some/file@branch' => link_to('source:/some/file@branch', source_url_with_branch, :class => 'source'),
350 'source:/some/file@branch' => link_to('source:/some/file@branch', source_url_with_branch, :class => 'source'),
351 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_rev_and_ext, :class => 'source'),
351 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_rev_and_ext, :class => 'source'),
352 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url + "#L110", :class => 'source'),
352 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url + "#L110", :class => 'source'),
353 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext + "#L110", :class => 'source'),
353 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext + "#L110", :class => 'source'),
354 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url_with_rev + "#L110", :class => 'source'),
354 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url_with_rev + "#L110", :class => 'source'),
355 # export
355 # export
356 'export:/some/file' => link_to('export:/some/file', export_url, :class => 'source download'),
356 'export:/some/file' => link_to('export:/some/file', export_url, :class => 'source download'),
357 'export:/some/file.ext' => link_to('export:/some/file.ext', export_url_with_ext, :class => 'source download'),
357 'export:/some/file.ext' => link_to('export:/some/file.ext', export_url_with_ext, :class => 'source download'),
358 'export:/some/file@52' => link_to('export:/some/file@52', export_url_with_rev, :class => 'source download'),
358 'export:/some/file@52' => link_to('export:/some/file@52', export_url_with_rev, :class => 'source download'),
359 'export:/some/file.ext@52' => link_to('export:/some/file.ext@52', export_url_with_rev_and_ext, :class => 'source download'),
359 'export:/some/file.ext@52' => link_to('export:/some/file.ext@52', export_url_with_rev_and_ext, :class => 'source download'),
360 'export:/some/file@branch' => link_to('export:/some/file@branch', export_url_with_branch, :class => 'source download'),
360 'export:/some/file@branch' => link_to('export:/some/file@branch', export_url_with_branch, :class => 'source download'),
361 # forum
361 # forum
362 'forum#2' => link_to('Discussion', board_url, :class => 'board'),
362 'forum#2' => link_to('Discussion', board_url, :class => 'board'),
363 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'),
363 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'),
364 # message
364 # message
365 'message#4' => link_to('Post 2', message_url, :class => 'message'),
365 'message#4' => link_to('Post 2', message_url, :class => 'message'),
366 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
366 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
367 # news
367 # news
368 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'),
368 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'),
369 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'),
369 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'),
370 # project
370 # project
371 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
371 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
372 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
372 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
373 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
373 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
374 # not found
374 # not found
375 '#0123456789' => '#0123456789',
375 '#0123456789' => '#0123456789',
376 # invalid expressions
376 # invalid expressions
377 'source:' => 'source:',
377 'source:' => 'source:',
378 # url hash
378 # url hash
379 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
379 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
380 }
380 }
381 @project = Project.find(1)
381 @project = Project.find(1)
382 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
382 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
383 end
383 end
384
384
385 def test_should_not_parse_redmine_links_inside_link
385 def test_should_not_parse_redmine_links_inside_link
386 raw = "r1 should not be parsed in http://example.com/url-r1/"
386 raw = "r1 should not be parsed in http://example.com/url-r1/"
387 assert_match %r{<p><a class="changeset".*>r1</a> should not be parsed in <a class="external" href="http://example.com/url-r1/">http://example.com/url-r1/</a></p>},
387 assert_match %r{<p><a class="changeset".*>r1</a> should not be parsed in <a class="external" href="http://example.com/url-r1/">http://example.com/url-r1/</a></p>},
388 textilizable(raw, :project => Project.find(1))
388 textilizable(raw, :project => Project.find(1))
389 end
389 end
390
390
391 def test_redmine_links_with_a_different_project_before_current_project
391 def test_redmine_links_with_a_different_project_before_current_project
392 vp1 = Version.generate!(:project_id => 1, :name => '1.4.4')
392 vp1 = Version.generate!(:project_id => 1, :name => '1.4.4')
393 vp3 = Version.generate!(:project_id => 3, :name => '1.4.4')
393 vp3 = Version.generate!(:project_id => 3, :name => '1.4.4')
394 @project = Project.find(3)
394 @project = Project.find(3)
395 result1 = link_to("1.4.4", "/versions/#{vp1.id}", :class => "version")
395 result1 = link_to("1.4.4", "/versions/#{vp1.id}", :class => "version")
396 result2 = link_to("1.4.4", "/versions/#{vp3.id}", :class => "version")
396 result2 = link_to("1.4.4", "/versions/#{vp3.id}", :class => "version")
397 assert_equal "<p>#{result1} #{result2}</p>",
397 assert_equal "<p>#{result1} #{result2}</p>",
398 textilizable("ecookbook:version:1.4.4 version:1.4.4")
398 textilizable("ecookbook:version:1.4.4 version:1.4.4")
399 end
399 end
400
400
401 def test_escaped_redmine_links_should_not_be_parsed
401 def test_escaped_redmine_links_should_not_be_parsed
402 to_test = [
402 to_test = [
403 '#3.',
403 '#3.',
404 '#3-14.',
404 '#3-14.',
405 '#3#-note14.',
405 '#3#-note14.',
406 'r1',
406 'r1',
407 'document#1',
407 'document#1',
408 'document:"Test document"',
408 'document:"Test document"',
409 'version#2',
409 'version#2',
410 'version:1.0',
410 'version:1.0',
411 'version:"1.0"',
411 'version:"1.0"',
412 'source:/some/file'
412 'source:/some/file'
413 ]
413 ]
414 @project = Project.find(1)
414 @project = Project.find(1)
415 to_test.each { |text| assert_equal "<p>#{text}</p>", textilizable("!" + text), "#{text} failed" }
415 to_test.each { |text| assert_equal "<p>#{text}</p>", textilizable("!" + text), "#{text} failed" }
416 end
416 end
417
417
418 def test_cross_project_redmine_links
418 def test_cross_project_redmine_links
419 source_link = link_to('ecookbook:source:/some/file',
419 source_link = link_to('ecookbook:source:/some/file',
420 {:controller => 'repositories', :action => 'entry',
420 {:controller => 'repositories', :action => 'entry',
421 :id => 'ecookbook', :path => ['some', 'file']},
421 :id => 'ecookbook', :path => ['some', 'file']},
422 :class => 'source')
422 :class => 'source')
423 changeset_link = link_to('ecookbook:r2',
423 changeset_link = link_to('ecookbook:r2',
424 {:controller => 'repositories', :action => 'revision',
424 {:controller => 'repositories', :action => 'revision',
425 :id => 'ecookbook', :rev => 2},
425 :id => 'ecookbook', :rev => 2},
426 :class => 'changeset',
426 :class => 'changeset',
427 :title => 'This commit fixes #1, #2 and references #1 & #3')
427 :title => 'This commit fixes #1, #2 and references #1 & #3')
428 to_test = {
428 to_test = {
429 # documents
429 # documents
430 'document:"Test document"' => 'document:"Test document"',
430 'document:"Test document"' => 'document:"Test document"',
431 'ecookbook:document:"Test document"' =>
431 'ecookbook:document:"Test document"' =>
432 link_to("Test document", "/documents/1", :class => "document"),
432 link_to("Test document", "/documents/1", :class => "document"),
433 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
433 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
434 # versions
434 # versions
435 'version:"1.0"' => 'version:"1.0"',
435 'version:"1.0"' => 'version:"1.0"',
436 'ecookbook:version:"1.0"' =>
436 'ecookbook:version:"1.0"' =>
437 link_to("1.0", "/versions/2", :class => "version"),
437 link_to("1.0", "/versions/2", :class => "version"),
438 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
438 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
439 # changeset
439 # changeset
440 'r2' => 'r2',
440 'r2' => 'r2',
441 'ecookbook:r2' => changeset_link,
441 'ecookbook:r2' => changeset_link,
442 'invalid:r2' => 'invalid:r2',
442 'invalid:r2' => 'invalid:r2',
443 # source
443 # source
444 'source:/some/file' => 'source:/some/file',
444 'source:/some/file' => 'source:/some/file',
445 'ecookbook:source:/some/file' => source_link,
445 'ecookbook:source:/some/file' => source_link,
446 'invalid:source:/some/file' => 'invalid:source:/some/file',
446 'invalid:source:/some/file' => 'invalid:source:/some/file',
447 }
447 }
448 @project = Project.find(3)
448 @project = Project.find(3)
449 to_test.each do |text, result|
449 to_test.each do |text, result|
450 assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed"
450 assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed"
451 end
451 end
452 end
452 end
453
453
454 def test_redmine_links_by_name_should_work_with_html_escaped_characters
454 def test_redmine_links_by_name_should_work_with_html_escaped_characters
455 v = Version.generate!(:name => "Test & Show.txt", :project_id => 1)
455 v = Version.generate!(:name => "Test & Show.txt", :project_id => 1)
456 link = link_to("Test & Show.txt", "/versions/#{v.id}", :class => "version")
456 link = link_to("Test & Show.txt", "/versions/#{v.id}", :class => "version")
457
457
458 @project = v.project
458 @project = v.project
459 assert_equal "<p>#{link}</p>", textilizable('version:"Test & Show.txt"')
459 assert_equal "<p>#{link}</p>", textilizable('version:"Test & Show.txt"')
460 end
460 end
461
461
462 def test_link_to_issue_subject
462 def test_link_to_issue_subject
463 issue = Issue.generate!(:subject => "01234567890123456789")
463 issue = Issue.generate!(:subject => "01234567890123456789")
464 str = link_to_issue(issue, :truncate => 10)
464 str = link_to_issue(issue, :truncate => 10)
465 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
465 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
466 assert_equal "#{result}: 0123456...", str
466 assert_equal "#{result}: 0123456...", str
467
467
468 issue = Issue.generate!(:subject => "<&>")
468 issue = Issue.generate!(:subject => "<&>")
469 str = link_to_issue(issue)
469 str = link_to_issue(issue)
470 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
470 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
471 assert_equal "#{result}: &lt;&amp;&gt;", str
471 assert_equal "#{result}: &lt;&amp;&gt;", str
472
472
473 issue = Issue.generate!(:subject => "<&>0123456789012345")
473 issue = Issue.generate!(:subject => "<&>0123456789012345")
474 str = link_to_issue(issue, :truncate => 10)
474 str = link_to_issue(issue, :truncate => 10)
475 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
475 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
476 assert_equal "#{result}: &lt;&amp;&gt;0123...", str
476 assert_equal "#{result}: &lt;&amp;&gt;0123...", str
477 end
477 end
478
478
479 def test_link_to_issue_title
479 def test_link_to_issue_title
480 long_str = "0123456789" * 5
480 long_str = "0123456789" * 5
481
481
482 issue = Issue.generate!(:subject => "#{long_str}01234567890123456789")
482 issue = Issue.generate!(:subject => "#{long_str}01234567890123456789")
483 str = link_to_issue(issue, :subject => false)
483 str = link_to_issue(issue, :subject => false)
484 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}",
484 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}",
485 :class => issue.css_classes,
485 :class => issue.css_classes,
486 :title => "#{long_str}0123456...")
486 :title => "#{long_str}0123456...")
487 assert_equal result, str
487 assert_equal result, str
488
488
489 issue = Issue.generate!(:subject => "<&>#{long_str}01234567890123456789")
489 issue = Issue.generate!(:subject => "<&>#{long_str}01234567890123456789")
490 str = link_to_issue(issue, :subject => false)
490 str = link_to_issue(issue, :subject => false)
491 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}",
491 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}",
492 :class => issue.css_classes,
492 :class => issue.css_classes,
493 :title => "<&>#{long_str}0123...")
493 :title => "<&>#{long_str}0123...")
494 assert_equal result, str
494 assert_equal result, str
495 end
495 end
496
496
497 def test_multiple_repositories_redmine_links
497 def test_multiple_repositories_redmine_links
498 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn_repo-1', :url => 'file:///foo/hg')
498 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn_repo-1', :url => 'file:///foo/hg')
499 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
499 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
500 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
500 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
501 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
501 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
502
502
503 changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
503 changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
504 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
504 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
505 svn_changeset_link = link_to('svn_repo-1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn_repo-1', :rev => 123},
505 svn_changeset_link = link_to('svn_repo-1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn_repo-1', :rev => 123},
506 :class => 'changeset', :title => '')
506 :class => 'changeset', :title => '')
507 hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
507 hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
508 :class => 'changeset', :title => '')
508 :class => 'changeset', :title => '')
509
509
510 source_link = link_to('source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
510 source_link = link_to('source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
511 hg_source_link = link_to('source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
511 hg_source_link = link_to('source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
512
512
513 to_test = {
513 to_test = {
514 'r2' => changeset_link,
514 'r2' => changeset_link,
515 'svn_repo-1|r123' => svn_changeset_link,
515 'svn_repo-1|r123' => svn_changeset_link,
516 'invalid|r123' => 'invalid|r123',
516 'invalid|r123' => 'invalid|r123',
517 'commit:hg1|abcd' => hg_changeset_link,
517 'commit:hg1|abcd' => hg_changeset_link,
518 'commit:invalid|abcd' => 'commit:invalid|abcd',
518 'commit:invalid|abcd' => 'commit:invalid|abcd',
519 # source
519 # source
520 'source:some/file' => source_link,
520 'source:some/file' => source_link,
521 'source:hg1|some/file' => hg_source_link,
521 'source:hg1|some/file' => hg_source_link,
522 'source:invalid|some/file' => 'source:invalid|some/file',
522 'source:invalid|some/file' => 'source:invalid|some/file',
523 }
523 }
524
524
525 @project = Project.find(1)
525 @project = Project.find(1)
526 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
526 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
527 end
527 end
528
528
529 def test_cross_project_multiple_repositories_redmine_links
529 def test_cross_project_multiple_repositories_redmine_links
530 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
530 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
531 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
531 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
532 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
532 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
533 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
533 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
534
534
535 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
535 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
536 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
536 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
537 svn_changeset_link = link_to('ecookbook:svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
537 svn_changeset_link = link_to('ecookbook:svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
538 :class => 'changeset', :title => '')
538 :class => 'changeset', :title => '')
539 hg_changeset_link = link_to('ecookbook:hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
539 hg_changeset_link = link_to('ecookbook:hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
540 :class => 'changeset', :title => '')
540 :class => 'changeset', :title => '')
541
541
542 source_link = link_to('ecookbook:source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
542 source_link = link_to('ecookbook:source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
543 hg_source_link = link_to('ecookbook:source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
543 hg_source_link = link_to('ecookbook:source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
544
544
545 to_test = {
545 to_test = {
546 'ecookbook:r2' => changeset_link,
546 'ecookbook:r2' => changeset_link,
547 'ecookbook:svn1|r123' => svn_changeset_link,
547 'ecookbook:svn1|r123' => svn_changeset_link,
548 'ecookbook:invalid|r123' => 'ecookbook:invalid|r123',
548 'ecookbook:invalid|r123' => 'ecookbook:invalid|r123',
549 'ecookbook:commit:hg1|abcd' => hg_changeset_link,
549 'ecookbook:commit:hg1|abcd' => hg_changeset_link,
550 'ecookbook:commit:invalid|abcd' => 'ecookbook:commit:invalid|abcd',
550 'ecookbook:commit:invalid|abcd' => 'ecookbook:commit:invalid|abcd',
551 'invalid:commit:invalid|abcd' => 'invalid:commit:invalid|abcd',
551 'invalid:commit:invalid|abcd' => 'invalid:commit:invalid|abcd',
552 # source
552 # source
553 'ecookbook:source:some/file' => source_link,
553 'ecookbook:source:some/file' => source_link,
554 'ecookbook:source:hg1|some/file' => hg_source_link,
554 'ecookbook:source:hg1|some/file' => hg_source_link,
555 'ecookbook:source:invalid|some/file' => 'ecookbook:source:invalid|some/file',
555 'ecookbook:source:invalid|some/file' => 'ecookbook:source:invalid|some/file',
556 'invalid:source:invalid|some/file' => 'invalid:source:invalid|some/file',
556 'invalid:source:invalid|some/file' => 'invalid:source:invalid|some/file',
557 }
557 }
558
558
559 @project = Project.find(3)
559 @project = Project.find(3)
560 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
560 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
561 end
561 end
562
562
563 def test_redmine_links_git_commit
563 def test_redmine_links_git_commit
564 changeset_link = link_to('abcd',
564 changeset_link = link_to('abcd',
565 {
565 {
566 :controller => 'repositories',
566 :controller => 'repositories',
567 :action => 'revision',
567 :action => 'revision',
568 :id => 'subproject1',
568 :id => 'subproject1',
569 :rev => 'abcd',
569 :rev => 'abcd',
570 },
570 },
571 :class => 'changeset', :title => 'test commit')
571 :class => 'changeset', :title => 'test commit')
572 to_test = {
572 to_test = {
573 'commit:abcd' => changeset_link,
573 'commit:abcd' => changeset_link,
574 }
574 }
575 @project = Project.find(3)
575 @project = Project.find(3)
576 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
576 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
577 assert r
577 assert r
578 c = Changeset.new(:repository => r,
578 c = Changeset.new(:repository => r,
579 :committed_on => Time.now,
579 :committed_on => Time.now,
580 :revision => 'abcd',
580 :revision => 'abcd',
581 :scmid => 'abcd',
581 :scmid => 'abcd',
582 :comments => 'test commit')
582 :comments => 'test commit')
583 assert( c.save )
583 assert( c.save )
584 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
584 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
585 end
585 end
586
586
587 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
587 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
588 def test_redmine_links_darcs_commit
588 def test_redmine_links_darcs_commit
589 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
589 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
590 {
590 {
591 :controller => 'repositories',
591 :controller => 'repositories',
592 :action => 'revision',
592 :action => 'revision',
593 :id => 'subproject1',
593 :id => 'subproject1',
594 :rev => '123',
594 :rev => '123',
595 },
595 },
596 :class => 'changeset', :title => 'test commit')
596 :class => 'changeset', :title => 'test commit')
597 to_test = {
597 to_test = {
598 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
598 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
599 }
599 }
600 @project = Project.find(3)
600 @project = Project.find(3)
601 r = Repository::Darcs.create!(
601 r = Repository::Darcs.create!(
602 :project => @project, :url => '/tmp/test/darcs',
602 :project => @project, :url => '/tmp/test/darcs',
603 :log_encoding => 'UTF-8')
603 :log_encoding => 'UTF-8')
604 assert r
604 assert r
605 c = Changeset.new(:repository => r,
605 c = Changeset.new(:repository => r,
606 :committed_on => Time.now,
606 :committed_on => Time.now,
607 :revision => '123',
607 :revision => '123',
608 :scmid => '20080308225258-98289-abcd456efg.gz',
608 :scmid => '20080308225258-98289-abcd456efg.gz',
609 :comments => 'test commit')
609 :comments => 'test commit')
610 assert( c.save )
610 assert( c.save )
611 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
611 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
612 end
612 end
613
613
614 def test_redmine_links_mercurial_commit
614 def test_redmine_links_mercurial_commit
615 changeset_link_rev = link_to('r123',
615 changeset_link_rev = link_to('r123',
616 {
616 {
617 :controller => 'repositories',
617 :controller => 'repositories',
618 :action => 'revision',
618 :action => 'revision',
619 :id => 'subproject1',
619 :id => 'subproject1',
620 :rev => '123' ,
620 :rev => '123' ,
621 },
621 },
622 :class => 'changeset', :title => 'test commit')
622 :class => 'changeset', :title => 'test commit')
623 changeset_link_commit = link_to('abcd',
623 changeset_link_commit = link_to('abcd',
624 {
624 {
625 :controller => 'repositories',
625 :controller => 'repositories',
626 :action => 'revision',
626 :action => 'revision',
627 :id => 'subproject1',
627 :id => 'subproject1',
628 :rev => 'abcd' ,
628 :rev => 'abcd' ,
629 },
629 },
630 :class => 'changeset', :title => 'test commit')
630 :class => 'changeset', :title => 'test commit')
631 to_test = {
631 to_test = {
632 'r123' => changeset_link_rev,
632 'r123' => changeset_link_rev,
633 'commit:abcd' => changeset_link_commit,
633 'commit:abcd' => changeset_link_commit,
634 }
634 }
635 @project = Project.find(3)
635 @project = Project.find(3)
636 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
636 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
637 assert r
637 assert r
638 c = Changeset.new(:repository => r,
638 c = Changeset.new(:repository => r,
639 :committed_on => Time.now,
639 :committed_on => Time.now,
640 :revision => '123',
640 :revision => '123',
641 :scmid => 'abcd',
641 :scmid => 'abcd',
642 :comments => 'test commit')
642 :comments => 'test commit')
643 assert( c.save )
643 assert( c.save )
644 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
644 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
645 end
645 end
646
646
647 def test_attachment_links
647 def test_attachment_links
648 text = 'attachment:error281.txt'
648 text = 'attachment:error281.txt'
649 result = link_to("error281.txt", "/attachments/download/1/error281.txt",
649 result = link_to("error281.txt", "/attachments/download/1/error281.txt",
650 :class => "attachment")
650 :class => "attachment")
651 assert_equal "<p>#{result}</p>",
651 assert_equal "<p>#{result}</p>",
652 textilizable(text,
652 textilizable(text,
653 :attachments => Issue.find(3).attachments),
653 :attachments => Issue.find(3).attachments),
654 "#{text} failed"
654 "#{text} failed"
655 end
655 end
656
656
657 def test_attachment_link_should_link_to_latest_attachment
657 def test_attachment_link_should_link_to_latest_attachment
658 set_tmp_attachments_directory
658 set_tmp_attachments_directory
659 a1 = Attachment.generate!(:filename => "test.txt", :created_on => 1.hour.ago)
659 a1 = Attachment.generate!(:filename => "test.txt", :created_on => 1.hour.ago)
660 a2 = Attachment.generate!(:filename => "test.txt")
660 a2 = Attachment.generate!(:filename => "test.txt")
661 result = link_to("test.txt", "/attachments/download/#{a2.id}/test.txt",
661 result = link_to("test.txt", "/attachments/download/#{a2.id}/test.txt",
662 :class => "attachment")
662 :class => "attachment")
663 assert_equal "<p>#{result}</p>",
663 assert_equal "<p>#{result}</p>",
664 textilizable('attachment:test.txt', :attachments => [a1, a2])
664 textilizable('attachment:test.txt', :attachments => [a1, a2])
665 end
665 end
666
666
667 def test_wiki_links
667 def test_wiki_links
668 russian_eacape = CGI.escape(@russian_test)
668 russian_eacape = CGI.escape(@russian_test)
669 to_test = {
669 to_test = {
670 '[[CookBook documentation]]' =>
670 '[[CookBook documentation]]' =>
671 link_to("CookBook documentation",
671 link_to("CookBook documentation",
672 "/projects/ecookbook/wiki/CookBook_documentation",
672 "/projects/ecookbook/wiki/CookBook_documentation",
673 :class => "wiki-page"),
673 :class => "wiki-page"),
674 '[[Another page|Page]]' =>
674 '[[Another page|Page]]' =>
675 link_to("Page",
675 link_to("Page",
676 "/projects/ecookbook/wiki/Another_page",
676 "/projects/ecookbook/wiki/Another_page",
677 :class => "wiki-page"),
677 :class => "wiki-page"),
678 # title content should be formatted
678 # title content should be formatted
679 '[[Another page|With _styled_ *title*]]' =>
679 '[[Another page|With _styled_ *title*]]' =>
680 link_to("With <em>styled</em> <strong>title</strong>".html_safe,
680 link_to("With <em>styled</em> <strong>title</strong>".html_safe,
681 "/projects/ecookbook/wiki/Another_page",
681 "/projects/ecookbook/wiki/Another_page",
682 :class => "wiki-page"),
682 :class => "wiki-page"),
683 '[[Another page|With title containing <strong>HTML entities &amp; markups</strong>]]' =>
683 '[[Another page|With title containing <strong>HTML entities &amp; markups</strong>]]' =>
684 link_to("With title containing &lt;strong&gt;HTML entities &amp; markups&lt;/strong&gt;".html_safe,
684 link_to("With title containing &lt;strong&gt;HTML entities &amp; markups&lt;/strong&gt;".html_safe,
685 "/projects/ecookbook/wiki/Another_page",
685 "/projects/ecookbook/wiki/Another_page",
686 :class => "wiki-page"),
686 :class => "wiki-page"),
687 # link with anchor
687 # link with anchor
688 '[[CookBook documentation#One-section]]' =>
688 '[[CookBook documentation#One-section]]' =>
689 link_to("CookBook documentation",
689 link_to("CookBook documentation",
690 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
690 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
691 :class => "wiki-page"),
691 :class => "wiki-page"),
692 '[[Another page#anchor|Page]]' =>
692 '[[Another page#anchor|Page]]' =>
693 link_to("Page",
693 link_to("Page",
694 "/projects/ecookbook/wiki/Another_page#anchor",
694 "/projects/ecookbook/wiki/Another_page#anchor",
695 :class => "wiki-page"),
695 :class => "wiki-page"),
696 # UTF8 anchor
696 # UTF8 anchor
697 "[[Another_page##{@russian_test}|#{@russian_test}]]" =>
697 "[[Another_page##{@russian_test}|#{@russian_test}]]" =>
698 link_to(@russian_test,
698 link_to(@russian_test,
699 "/projects/ecookbook/wiki/Another_page##{russian_eacape}",
699 "/projects/ecookbook/wiki/Another_page##{russian_eacape}",
700 :class => "wiki-page"),
700 :class => "wiki-page"),
701 # page that doesn't exist
701 # page that doesn't exist
702 '[[Unknown page]]' =>
702 '[[Unknown page]]' =>
703 link_to("Unknown page",
703 link_to("Unknown page",
704 "/projects/ecookbook/wiki/Unknown_page",
704 "/projects/ecookbook/wiki/Unknown_page",
705 :class => "wiki-page new"),
705 :class => "wiki-page new"),
706 '[[Unknown page|404]]' =>
706 '[[Unknown page|404]]' =>
707 link_to("404",
707 link_to("404",
708 "/projects/ecookbook/wiki/Unknown_page",
708 "/projects/ecookbook/wiki/Unknown_page",
709 :class => "wiki-page new"),
709 :class => "wiki-page new"),
710 # link to another project wiki
710 # link to another project wiki
711 '[[onlinestore:]]' =>
711 '[[onlinestore:]]' =>
712 link_to("onlinestore",
712 link_to("onlinestore",
713 "/projects/onlinestore/wiki",
713 "/projects/onlinestore/wiki",
714 :class => "wiki-page"),
714 :class => "wiki-page"),
715 '[[onlinestore:|Wiki]]' =>
715 '[[onlinestore:|Wiki]]' =>
716 link_to("Wiki",
716 link_to("Wiki",
717 "/projects/onlinestore/wiki",
717 "/projects/onlinestore/wiki",
718 :class => "wiki-page"),
718 :class => "wiki-page"),
719 '[[onlinestore:Start page]]' =>
719 '[[onlinestore:Start page]]' =>
720 link_to("Start page",
720 link_to("Start page",
721 "/projects/onlinestore/wiki/Start_page",
721 "/projects/onlinestore/wiki/Start_page",
722 :class => "wiki-page"),
722 :class => "wiki-page"),
723 '[[onlinestore:Start page|Text]]' =>
723 '[[onlinestore:Start page|Text]]' =>
724 link_to("Text",
724 link_to("Text",
725 "/projects/onlinestore/wiki/Start_page",
725 "/projects/onlinestore/wiki/Start_page",
726 :class => "wiki-page"),
726 :class => "wiki-page"),
727 '[[onlinestore:Unknown page]]' =>
727 '[[onlinestore:Unknown page]]' =>
728 link_to("Unknown page",
728 link_to("Unknown page",
729 "/projects/onlinestore/wiki/Unknown_page",
729 "/projects/onlinestore/wiki/Unknown_page",
730 :class => "wiki-page new"),
730 :class => "wiki-page new"),
731 # struck through link
731 # struck through link
732 '-[[Another page|Page]]-' =>
732 '-[[Another page|Page]]-' =>
733 "<del>".html_safe +
733 "<del>".html_safe +
734 link_to("Page",
734 link_to("Page",
735 "/projects/ecookbook/wiki/Another_page",
735 "/projects/ecookbook/wiki/Another_page",
736 :class => "wiki-page").html_safe +
736 :class => "wiki-page").html_safe +
737 "</del>".html_safe,
737 "</del>".html_safe,
738 '-[[Another page|Page]] link-' =>
738 '-[[Another page|Page]] link-' =>
739 "<del>".html_safe +
739 "<del>".html_safe +
740 link_to("Page",
740 link_to("Page",
741 "/projects/ecookbook/wiki/Another_page",
741 "/projects/ecookbook/wiki/Another_page",
742 :class => "wiki-page").html_safe +
742 :class => "wiki-page").html_safe +
743 " link</del>".html_safe,
743 " link</del>".html_safe,
744 # escaping
744 # escaping
745 '![[Another page|Page]]' => '[[Another page|Page]]',
745 '![[Another page|Page]]' => '[[Another page|Page]]',
746 # project does not exist
746 # project does not exist
747 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
747 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
748 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
748 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
749 }
749 }
750 @project = Project.find(1)
750 @project = Project.find(1)
751 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
751 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
752 end
752 end
753
753
754 def test_wiki_links_within_local_file_generation_context
754 def test_wiki_links_within_local_file_generation_context
755 to_test = {
755 to_test = {
756 # link to a page
756 # link to a page
757 '[[CookBook documentation]]' =>
757 '[[CookBook documentation]]' =>
758 link_to("CookBook documentation", "CookBook_documentation.html",
758 link_to("CookBook documentation", "CookBook_documentation.html",
759 :class => "wiki-page"),
759 :class => "wiki-page"),
760 '[[CookBook documentation|documentation]]' =>
760 '[[CookBook documentation|documentation]]' =>
761 link_to("documentation", "CookBook_documentation.html",
761 link_to("documentation", "CookBook_documentation.html",
762 :class => "wiki-page"),
762 :class => "wiki-page"),
763 '[[CookBook documentation#One-section]]' =>
763 '[[CookBook documentation#One-section]]' =>
764 link_to("CookBook documentation", "CookBook_documentation.html#One-section",
764 link_to("CookBook documentation", "CookBook_documentation.html#One-section",
765 :class => "wiki-page"),
765 :class => "wiki-page"),
766 '[[CookBook documentation#One-section|documentation]]' =>
766 '[[CookBook documentation#One-section|documentation]]' =>
767 link_to("documentation", "CookBook_documentation.html#One-section",
767 link_to("documentation", "CookBook_documentation.html#One-section",
768 :class => "wiki-page"),
768 :class => "wiki-page"),
769 # page that doesn't exist
769 # page that doesn't exist
770 '[[Unknown page]]' =>
770 '[[Unknown page]]' =>
771 link_to("Unknown page", "Unknown_page.html",
771 link_to("Unknown page", "Unknown_page.html",
772 :class => "wiki-page new"),
772 :class => "wiki-page new"),
773 '[[Unknown page|404]]' =>
773 '[[Unknown page|404]]' =>
774 link_to("404", "Unknown_page.html",
774 link_to("404", "Unknown_page.html",
775 :class => "wiki-page new"),
775 :class => "wiki-page new"),
776 '[[Unknown page#anchor]]' =>
776 '[[Unknown page#anchor]]' =>
777 link_to("Unknown page", "Unknown_page.html#anchor",
777 link_to("Unknown page", "Unknown_page.html#anchor",
778 :class => "wiki-page new"),
778 :class => "wiki-page new"),
779 '[[Unknown page#anchor|404]]' =>
779 '[[Unknown page#anchor|404]]' =>
780 link_to("404", "Unknown_page.html#anchor",
780 link_to("404", "Unknown_page.html#anchor",
781 :class => "wiki-page new"),
781 :class => "wiki-page new"),
782 }
782 }
783 @project = Project.find(1)
783 @project = Project.find(1)
784 to_test.each do |text, result|
784 to_test.each do |text, result|
785 assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local)
785 assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local)
786 end
786 end
787 end
787 end
788
788
789 def test_wiki_links_within_wiki_page_context
789 def test_wiki_links_within_wiki_page_context
790 page = WikiPage.find_by_title('Another_page' )
790 page = WikiPage.find_by_title('Another_page' )
791 to_test = {
791 to_test = {
792 '[[CookBook documentation]]' =>
792 '[[CookBook documentation]]' =>
793 link_to("CookBook documentation",
793 link_to("CookBook documentation",
794 "/projects/ecookbook/wiki/CookBook_documentation",
794 "/projects/ecookbook/wiki/CookBook_documentation",
795 :class => "wiki-page"),
795 :class => "wiki-page"),
796 '[[CookBook documentation|documentation]]' =>
796 '[[CookBook documentation|documentation]]' =>
797 link_to("documentation",
797 link_to("documentation",
798 "/projects/ecookbook/wiki/CookBook_documentation",
798 "/projects/ecookbook/wiki/CookBook_documentation",
799 :class => "wiki-page"),
799 :class => "wiki-page"),
800 '[[CookBook documentation#One-section]]' =>
800 '[[CookBook documentation#One-section]]' =>
801 link_to("CookBook documentation",
801 link_to("CookBook documentation",
802 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
802 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
803 :class => "wiki-page"),
803 :class => "wiki-page"),
804 '[[CookBook documentation#One-section|documentation]]' =>
804 '[[CookBook documentation#One-section|documentation]]' =>
805 link_to("documentation",
805 link_to("documentation",
806 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
806 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
807 :class => "wiki-page"),
807 :class => "wiki-page"),
808 # link to the current page
808 # link to the current page
809 '[[Another page]]' =>
809 '[[Another page]]' =>
810 link_to("Another page",
810 link_to("Another page",
811 "/projects/ecookbook/wiki/Another_page",
811 "/projects/ecookbook/wiki/Another_page",
812 :class => "wiki-page"),
812 :class => "wiki-page"),
813 '[[Another page|Page]]' =>
813 '[[Another page|Page]]' =>
814 link_to("Page",
814 link_to("Page",
815 "/projects/ecookbook/wiki/Another_page",
815 "/projects/ecookbook/wiki/Another_page",
816 :class => "wiki-page"),
816 :class => "wiki-page"),
817 '[[Another page#anchor]]' =>
817 '[[Another page#anchor]]' =>
818 link_to("Another page",
818 link_to("Another page",
819 "#anchor",
819 "#anchor",
820 :class => "wiki-page"),
820 :class => "wiki-page"),
821 '[[Another page#anchor|Page]]' =>
821 '[[Another page#anchor|Page]]' =>
822 link_to("Page",
822 link_to("Page",
823 "#anchor",
823 "#anchor",
824 :class => "wiki-page"),
824 :class => "wiki-page"),
825 # page that doesn't exist
825 # page that doesn't exist
826 '[[Unknown page]]' =>
826 '[[Unknown page]]' =>
827 link_to("Unknown page",
827 link_to("Unknown page",
828 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page",
828 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page",
829 :class => "wiki-page new"),
829 :class => "wiki-page new"),
830 '[[Unknown page|404]]' =>
830 '[[Unknown page|404]]' =>
831 link_to("404",
831 link_to("404",
832 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page",
832 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page",
833 :class => "wiki-page new"),
833 :class => "wiki-page new"),
834 '[[Unknown page#anchor]]' =>
834 '[[Unknown page#anchor]]' =>
835 link_to("Unknown page",
835 link_to("Unknown page",
836 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor",
836 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor",
837 :class => "wiki-page new"),
837 :class => "wiki-page new"),
838 '[[Unknown page#anchor|404]]' =>
838 '[[Unknown page#anchor|404]]' =>
839 link_to("404",
839 link_to("404",
840 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor",
840 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor",
841 :class => "wiki-page new"),
841 :class => "wiki-page new"),
842 }
842 }
843 @project = Project.find(1)
843 @project = Project.find(1)
844 to_test.each do |text, result|
844 to_test.each do |text, result|
845 assert_equal "<p>#{result}</p>",
845 assert_equal "<p>#{result}</p>",
846 textilizable(WikiContent.new( :text => text, :page => page ), :text)
846 textilizable(WikiContent.new( :text => text, :page => page ), :text)
847 end
847 end
848 end
848 end
849
849
850 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
850 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
851 to_test = {
851 to_test = {
852 # link to a page
852 # link to a page
853 '[[CookBook documentation]]' =>
853 '[[CookBook documentation]]' =>
854 link_to("CookBook documentation",
854 link_to("CookBook documentation",
855 "#CookBook_documentation",
855 "#CookBook_documentation",
856 :class => "wiki-page"),
856 :class => "wiki-page"),
857 '[[CookBook documentation|documentation]]' =>
857 '[[CookBook documentation|documentation]]' =>
858 link_to("documentation",
858 link_to("documentation",
859 "#CookBook_documentation",
859 "#CookBook_documentation",
860 :class => "wiki-page"),
860 :class => "wiki-page"),
861 '[[CookBook documentation#One-section]]' =>
861 '[[CookBook documentation#One-section]]' =>
862 link_to("CookBook documentation",
862 link_to("CookBook documentation",
863 "#CookBook_documentation_One-section",
863 "#CookBook_documentation_One-section",
864 :class => "wiki-page"),
864 :class => "wiki-page"),
865 '[[CookBook documentation#One-section|documentation]]' =>
865 '[[CookBook documentation#One-section|documentation]]' =>
866 link_to("documentation",
866 link_to("documentation",
867 "#CookBook_documentation_One-section",
867 "#CookBook_documentation_One-section",
868 :class => "wiki-page"),
868 :class => "wiki-page"),
869 # page that doesn't exist
869 # page that doesn't exist
870 '[[Unknown page]]' =>
870 '[[Unknown page]]' =>
871 link_to("Unknown page",
871 link_to("Unknown page",
872 "#Unknown_page",
872 "#Unknown_page",
873 :class => "wiki-page new"),
873 :class => "wiki-page new"),
874 '[[Unknown page|404]]' =>
874 '[[Unknown page|404]]' =>
875 link_to("404",
875 link_to("404",
876 "#Unknown_page",
876 "#Unknown_page",
877 :class => "wiki-page new"),
877 :class => "wiki-page new"),
878 '[[Unknown page#anchor]]' =>
878 '[[Unknown page#anchor]]' =>
879 link_to("Unknown page",
879 link_to("Unknown page",
880 "#Unknown_page_anchor",
880 "#Unknown_page_anchor",
881 :class => "wiki-page new"),
881 :class => "wiki-page new"),
882 '[[Unknown page#anchor|404]]' =>
882 '[[Unknown page#anchor|404]]' =>
883 link_to("404",
883 link_to("404",
884 "#Unknown_page_anchor",
884 "#Unknown_page_anchor",
885 :class => "wiki-page new"),
885 :class => "wiki-page new"),
886 }
886 }
887 @project = Project.find(1)
887 @project = Project.find(1)
888 to_test.each do |text, result|
888 to_test.each do |text, result|
889 assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor)
889 assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor)
890 end
890 end
891 end
891 end
892
892
893 def test_html_tags
893 def test_html_tags
894 to_test = {
894 to_test = {
895 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
895 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
896 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
896 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
897 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
897 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
898 # do not escape pre/code tags
898 # do not escape pre/code tags
899 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
899 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
900 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
900 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
901 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
901 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
902 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
902 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
903 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
903 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
904 # remove attributes except class
904 # remove attributes except class
905 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
905 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
906 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
906 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
907 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
907 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
908 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
908 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
909 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
909 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
910 # xss
910 # xss
911 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
911 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
912 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
912 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
913 }
913 }
914 to_test.each { |text, result| assert_equal result, textilizable(text) }
914 to_test.each { |text, result| assert_equal result, textilizable(text) }
915 end
915 end
916
916
917 def test_allowed_html_tags
917 def test_allowed_html_tags
918 to_test = {
918 to_test = {
919 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
919 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
920 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
920 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
921 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
921 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
922 }
922 }
923 to_test.each { |text, result| assert_equal result, textilizable(text) }
923 to_test.each { |text, result| assert_equal result, textilizable(text) }
924 end
924 end
925
925
926 def test_pre_tags
926 def test_pre_tags
927 raw = <<-RAW
927 raw = <<-RAW
928 Before
928 Before
929
929
930 <pre>
930 <pre>
931 <prepared-statement-cache-size>32</prepared-statement-cache-size>
931 <prepared-statement-cache-size>32</prepared-statement-cache-size>
932 </pre>
932 </pre>
933
933
934 After
934 After
935 RAW
935 RAW
936
936
937 expected = <<-EXPECTED
937 expected = <<-EXPECTED
938 <p>Before</p>
938 <p>Before</p>
939 <pre>
939 <pre>
940 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
940 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
941 </pre>
941 </pre>
942 <p>After</p>
942 <p>After</p>
943 EXPECTED
943 EXPECTED
944
944
945 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
945 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
946 end
946 end
947
947
948 def test_pre_content_should_not_parse_wiki_and_redmine_links
948 def test_pre_content_should_not_parse_wiki_and_redmine_links
949 raw = <<-RAW
949 raw = <<-RAW
950 [[CookBook documentation]]
950 [[CookBook documentation]]
951
951
952 #1
952 #1
953
953
954 <pre>
954 <pre>
955 [[CookBook documentation]]
955 [[CookBook documentation]]
956
956
957 #1
957 #1
958 </pre>
958 </pre>
959 RAW
959 RAW
960
960
961 result1 = link_to("CookBook documentation",
961 result1 = link_to("CookBook documentation",
962 "/projects/ecookbook/wiki/CookBook_documentation",
962 "/projects/ecookbook/wiki/CookBook_documentation",
963 :class => "wiki-page")
963 :class => "wiki-page")
964 result2 = link_to('#1',
964 result2 = link_to('#1',
965 "/issues/1",
965 "/issues/1",
966 :class => Issue.find(1).css_classes,
966 :class => Issue.find(1).css_classes,
967 :title => "Bug: Cannot print recipes (New)")
967 :title => "Bug: Cannot print recipes (New)")
968
968
969 expected = <<-EXPECTED
969 expected = <<-EXPECTED
970 <p>#{result1}</p>
970 <p>#{result1}</p>
971 <p>#{result2}</p>
971 <p>#{result2}</p>
972 <pre>
972 <pre>
973 [[CookBook documentation]]
973 [[CookBook documentation]]
974
974
975 #1
975 #1
976 </pre>
976 </pre>
977 EXPECTED
977 EXPECTED
978
978
979 @project = Project.find(1)
979 @project = Project.find(1)
980 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
980 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
981 end
981 end
982
982
983 def test_non_closing_pre_blocks_should_be_closed
983 def test_non_closing_pre_blocks_should_be_closed
984 raw = <<-RAW
984 raw = <<-RAW
985 <pre><code>
985 <pre><code>
986 RAW
986 RAW
987
987
988 expected = <<-EXPECTED
988 expected = <<-EXPECTED
989 <pre><code>
989 <pre><code>
990 </code></pre>
990 </code></pre>
991 EXPECTED
991 EXPECTED
992
992
993 @project = Project.find(1)
993 @project = Project.find(1)
994 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
994 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
995 end
995 end
996
996
997 def test_unbalanced_closing_pre_tag_should_not_error
997 def test_unbalanced_closing_pre_tag_should_not_error
998 assert_nothing_raised do
998 assert_nothing_raised do
999 textilizable("unbalanced</pre>")
999 textilizable("unbalanced</pre>")
1000 end
1000 end
1001 end
1001 end
1002
1002
1003 def test_syntax_highlight
1003 def test_syntax_highlight
1004 raw = <<-RAW
1004 raw = <<-RAW
1005 <pre><code class="ruby">
1005 <pre><code class="ruby">
1006 # Some ruby code here
1006 # Some ruby code here
1007 </code></pre>
1007 </code></pre>
1008 RAW
1008 RAW
1009
1009
1010 expected = <<-EXPECTED
1010 expected = <<-EXPECTED
1011 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="comment"># Some ruby code here</span></span>
1011 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="comment"># Some ruby code here</span></span>
1012 </code></pre>
1012 </code></pre>
1013 EXPECTED
1013 EXPECTED
1014
1014
1015 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
1015 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
1016 end
1016 end
1017
1017
1018 def test_to_path_param
1018 def test_to_path_param
1019 assert_equal 'test1/test2', to_path_param('test1/test2')
1019 assert_equal 'test1/test2', to_path_param('test1/test2')
1020 assert_equal 'test1/test2', to_path_param('/test1/test2/')
1020 assert_equal 'test1/test2', to_path_param('/test1/test2/')
1021 assert_equal 'test1/test2', to_path_param('//test1/test2/')
1021 assert_equal 'test1/test2', to_path_param('//test1/test2/')
1022 assert_equal nil, to_path_param('/')
1022 assert_equal nil, to_path_param('/')
1023 end
1023 end
1024
1024
1025 def test_wiki_links_in_tables
1025 def test_wiki_links_in_tables
1026 text = "|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|"
1026 text = "|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|"
1027 link1 = link_to("Link title", "/projects/ecookbook/wiki/Page", :class => "wiki-page new")
1027 link1 = link_to("Link title", "/projects/ecookbook/wiki/Page", :class => "wiki-page new")
1028 link2 = link_to("Other title", "/projects/ecookbook/wiki/Other_Page", :class => "wiki-page new")
1028 link2 = link_to("Other title", "/projects/ecookbook/wiki/Other_Page", :class => "wiki-page new")
1029 link3 = link_to("Last page", "/projects/ecookbook/wiki/Last_page", :class => "wiki-page new")
1029 link3 = link_to("Last page", "/projects/ecookbook/wiki/Last_page", :class => "wiki-page new")
1030 result = "<tr><td>#{link1}</td>" +
1030 result = "<tr><td>#{link1}</td>" +
1031 "<td>#{link2}</td>" +
1031 "<td>#{link2}</td>" +
1032 "</tr><tr><td>Cell 21</td><td>#{link3}</td></tr>"
1032 "</tr><tr><td>Cell 21</td><td>#{link3}</td></tr>"
1033 @project = Project.find(1)
1033 @project = Project.find(1)
1034 assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '')
1034 assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '')
1035 end
1035 end
1036
1036
1037 def test_text_formatting
1037 def test_text_formatting
1038 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
1038 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
1039 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
1039 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
1040 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
1040 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
1041 'a H *umane* W *eb* T *ext* G *enerator*' => 'a H <strong>umane</strong> W <strong>eb</strong> T <strong>ext</strong> G <strong>enerator</strong>',
1041 'a H *umane* W *eb* T *ext* G *enerator*' => 'a H <strong>umane</strong> W <strong>eb</strong> T <strong>ext</strong> G <strong>enerator</strong>',
1042 'a *H* umane *W* eb *T* ext *G* enerator' => 'a <strong>H</strong> umane <strong>W</strong> eb <strong>T</strong> ext <strong>G</strong> enerator',
1042 'a *H* umane *W* eb *T* ext *G* enerator' => 'a <strong>H</strong> umane <strong>W</strong> eb <strong>T</strong> ext <strong>G</strong> enerator',
1043 }
1043 }
1044 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
1044 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
1045 end
1045 end
1046
1046
1047 def test_wiki_horizontal_rule
1047 def test_wiki_horizontal_rule
1048 assert_equal '<hr />', textilizable('---')
1048 assert_equal '<hr />', textilizable('---')
1049 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
1049 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
1050 end
1050 end
1051
1051
1052 def test_footnotes
1052 def test_footnotes
1053 raw = <<-RAW
1053 raw = <<-RAW
1054 This is some text[1].
1054 This is some text[1].
1055
1055
1056 fn1. This is the foot note
1056 fn1. This is the foot note
1057 RAW
1057 RAW
1058
1058
1059 expected = <<-EXPECTED
1059 expected = <<-EXPECTED
1060 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
1060 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
1061 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
1061 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
1062 EXPECTED
1062 EXPECTED
1063
1063
1064 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
1064 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
1065 end
1065 end
1066
1066
1067 def test_headings
1067 def test_headings
1068 raw = 'h1. Some heading'
1068 raw = 'h1. Some heading'
1069 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
1069 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
1070
1070
1071 assert_equal expected, textilizable(raw)
1071 assert_equal expected, textilizable(raw)
1072 end
1072 end
1073
1073
1074 def test_headings_with_special_chars
1074 def test_headings_with_special_chars
1075 # This test makes sure that the generated anchor names match the expected
1075 # This test makes sure that the generated anchor names match the expected
1076 # ones even if the heading text contains unconventional characters
1076 # ones even if the heading text contains unconventional characters
1077 raw = 'h1. Some heading related to version 0.5'
1077 raw = 'h1. Some heading related to version 0.5'
1078 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
1078 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
1079 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
1079 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
1080
1080
1081 assert_equal expected, textilizable(raw)
1081 assert_equal expected, textilizable(raw)
1082 end
1082 end
1083
1083
1084 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
1084 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
1085 page = WikiPage.new( :title => 'Page Title', :wiki_id => 1 )
1085 page = WikiPage.new( :title => 'Page Title', :wiki_id => 1 )
1086 content = WikiContent.new( :text => 'h1. Some heading', :page => page )
1086 content = WikiContent.new( :text => 'h1. Some heading', :page => page )
1087
1087
1088 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
1088 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
1089
1089
1090 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
1090 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
1091 end
1091 end
1092
1092
1093 def test_table_of_content
1093 def test_table_of_content
1094 raw = <<-RAW
1094 raw = <<-RAW
1095 {{toc}}
1095 {{toc}}
1096
1096
1097 h1. Title
1097 h1. Title
1098
1098
1099 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1099 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1100
1100
1101 h2. Subtitle with a [[Wiki]] link
1101 h2. Subtitle with a [[Wiki]] link
1102
1102
1103 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
1103 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
1104
1104
1105 h2. Subtitle with [[Wiki|another Wiki]] link
1105 h2. Subtitle with [[Wiki|another Wiki]] link
1106
1106
1107 h2. Subtitle with %{color:red}red text%
1107 h2. Subtitle with %{color:red}red text%
1108
1108
1109 <pre>
1109 <pre>
1110 some code
1110 some code
1111 </pre>
1111 </pre>
1112
1112
1113 h3. Subtitle with *some* _modifiers_
1113 h3. Subtitle with *some* _modifiers_
1114
1114
1115 h3. Subtitle with @inline code@
1115 h3. Subtitle with @inline code@
1116
1116
1117 h1. Another title
1117 h1. Another title
1118
1118
1119 h3. An "Internet link":http://www.redmine.org/ inside subtitle
1119 h3. An "Internet link":http://www.redmine.org/ inside subtitle
1120
1120
1121 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
1121 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
1122
1122
1123 RAW
1123 RAW
1124
1124
1125 expected = '<ul class="toc">' +
1125 expected = '<ul class="toc">' +
1126 '<li><a href="#Title">Title</a>' +
1126 '<li><a href="#Title">Title</a>' +
1127 '<ul>' +
1127 '<ul>' +
1128 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
1128 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
1129 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
1129 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
1130 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
1130 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
1131 '<ul>' +
1131 '<ul>' +
1132 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
1132 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
1133 '<li><a href="#Subtitle-with-inline-code">Subtitle with inline code</a></li>' +
1133 '<li><a href="#Subtitle-with-inline-code">Subtitle with inline code</a></li>' +
1134 '</ul>' +
1134 '</ul>' +
1135 '</li>' +
1135 '</li>' +
1136 '</ul>' +
1136 '</ul>' +
1137 '</li>' +
1137 '</li>' +
1138 '<li><a href="#Another-title">Another title</a>' +
1138 '<li><a href="#Another-title">Another title</a>' +
1139 '<ul>' +
1139 '<ul>' +
1140 '<li>' +
1140 '<li>' +
1141 '<ul>' +
1141 '<ul>' +
1142 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
1142 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
1143 '</ul>' +
1143 '</ul>' +
1144 '</li>' +
1144 '</li>' +
1145 '<li><a href="#Project-Name">Project Name</a></li>' +
1145 '<li><a href="#Project-Name">Project Name</a></li>' +
1146 '</ul>' +
1146 '</ul>' +
1147 '</li>' +
1147 '</li>' +
1148 '</ul>'
1148 '</ul>'
1149
1149
1150 @project = Project.find(1)
1150 @project = Project.find(1)
1151 assert textilizable(raw).gsub("\n", "").include?(expected)
1151 assert textilizable(raw).gsub("\n", "").include?(expected)
1152 end
1152 end
1153
1153
1154 def test_table_of_content_should_generate_unique_anchors
1154 def test_table_of_content_should_generate_unique_anchors
1155 raw = <<-RAW
1155 raw = <<-RAW
1156 {{toc}}
1156 {{toc}}
1157
1157
1158 h1. Title
1158 h1. Title
1159
1159
1160 h2. Subtitle
1160 h2. Subtitle
1161
1161
1162 h2. Subtitle
1162 h2. Subtitle
1163 RAW
1163 RAW
1164
1164
1165 expected = '<ul class="toc">' +
1165 expected = '<ul class="toc">' +
1166 '<li><a href="#Title">Title</a>' +
1166 '<li><a href="#Title">Title</a>' +
1167 '<ul>' +
1167 '<ul>' +
1168 '<li><a href="#Subtitle">Subtitle</a></li>' +
1168 '<li><a href="#Subtitle">Subtitle</a></li>' +
1169 '<li><a href="#Subtitle-2">Subtitle</a></li>' +
1169 '<li><a href="#Subtitle-2">Subtitle</a></li>' +
1170 '</ul>' +
1170 '</ul>' +
1171 '</li>' +
1171 '</li>' +
1172 '</ul>'
1172 '</ul>'
1173
1173
1174 @project = Project.find(1)
1174 @project = Project.find(1)
1175 result = textilizable(raw).gsub("\n", "")
1175 result = textilizable(raw).gsub("\n", "")
1176 assert_include expected, result
1176 assert_include expected, result
1177 assert_include '<a name="Subtitle">', result
1177 assert_include '<a name="Subtitle">', result
1178 assert_include '<a name="Subtitle-2">', result
1178 assert_include '<a name="Subtitle-2">', result
1179 end
1179 end
1180
1180
1181 def test_table_of_content_should_contain_included_page_headings
1181 def test_table_of_content_should_contain_included_page_headings
1182 raw = <<-RAW
1182 raw = <<-RAW
1183 {{toc}}
1183 {{toc}}
1184
1184
1185 h1. Included
1185 h1. Included
1186
1186
1187 {{include(Child_1)}}
1187 {{include(Child_1)}}
1188 RAW
1188 RAW
1189
1189
1190 expected = '<ul class="toc">' +
1190 expected = '<ul class="toc">' +
1191 '<li><a href="#Included">Included</a></li>' +
1191 '<li><a href="#Included">Included</a></li>' +
1192 '<li><a href="#Child-page-1">Child page 1</a></li>' +
1192 '<li><a href="#Child-page-1">Child page 1</a></li>' +
1193 '</ul>'
1193 '</ul>'
1194
1194
1195 @project = Project.find(1)
1195 @project = Project.find(1)
1196 assert textilizable(raw).gsub("\n", "").include?(expected)
1196 assert textilizable(raw).gsub("\n", "").include?(expected)
1197 end
1197 end
1198
1198
1199 def test_toc_with_textile_formatting_should_be_parsed
1199 def test_toc_with_textile_formatting_should_be_parsed
1200 with_settings :text_formatting => 'textile' do
1200 with_settings :text_formatting => 'textile' do
1201 assert_select_in textilizable("{{toc}}\n\nh1. Heading"), 'ul.toc li', :text => 'Heading'
1201 assert_select_in textilizable("{{toc}}\n\nh1. Heading"), 'ul.toc li', :text => 'Heading'
1202 assert_select_in textilizable("{{<toc}}\n\nh1. Heading"), 'ul.toc.left li', :text => 'Heading'
1202 assert_select_in textilizable("{{<toc}}\n\nh1. Heading"), 'ul.toc.left li', :text => 'Heading'
1203 assert_select_in textilizable("{{>toc}}\n\nh1. Heading"), 'ul.toc.right li', :text => 'Heading'
1203 assert_select_in textilizable("{{>toc}}\n\nh1. Heading"), 'ul.toc.right li', :text => 'Heading'
1204 end
1204 end
1205 end
1205 end
1206
1206
1207 if Object.const_defined?(:Redcarpet)
1207 if Object.const_defined?(:Redcarpet)
1208 def test_toc_with_markdown_formatting_should_be_parsed
1208 def test_toc_with_markdown_formatting_should_be_parsed
1209 with_settings :text_formatting => 'markdown' do
1209 with_settings :text_formatting => 'markdown' do
1210 assert_select_in textilizable("{{toc}}\n\n# Heading"), 'ul.toc li', :text => 'Heading'
1210 assert_select_in textilizable("{{toc}}\n\n# Heading"), 'ul.toc li', :text => 'Heading'
1211 assert_select_in textilizable("{{<toc}}\n\n# Heading"), 'ul.toc.left li', :text => 'Heading'
1211 assert_select_in textilizable("{{<toc}}\n\n# Heading"), 'ul.toc.left li', :text => 'Heading'
1212 assert_select_in textilizable("{{>toc}}\n\n# Heading"), 'ul.toc.right li', :text => 'Heading'
1212 assert_select_in textilizable("{{>toc}}\n\n# Heading"), 'ul.toc.right li', :text => 'Heading'
1213 end
1213 end
1214 end
1214 end
1215 end
1215 end
1216
1216
1217 def test_section_edit_links
1217 def test_section_edit_links
1218 raw = <<-RAW
1218 raw = <<-RAW
1219 h1. Title
1219 h1. Title
1220
1220
1221 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1221 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1222
1222
1223 h2. Subtitle with a [[Wiki]] link
1223 h2. Subtitle with a [[Wiki]] link
1224
1224
1225 h2. Subtitle with *some* _modifiers_
1225 h2. Subtitle with *some* _modifiers_
1226
1226
1227 h2. Subtitle with @inline code@
1227 h2. Subtitle with @inline code@
1228
1228
1229 <pre>
1229 <pre>
1230 some code
1230 some code
1231
1231
1232 h2. heading inside pre
1232 h2. heading inside pre
1233
1233
1234 <h2>html heading inside pre</h2>
1234 <h2>html heading inside pre</h2>
1235 </pre>
1235 </pre>
1236
1236
1237 h2. Subtitle after pre tag
1237 h2. Subtitle after pre tag
1238 RAW
1238 RAW
1239
1239
1240 @project = Project.find(1)
1240 @project = Project.find(1)
1241 set_language_if_valid 'en'
1241 set_language_if_valid 'en'
1242 result = textilizable(raw, :edit_section_links => {:controller => 'wiki', :action => 'edit', :project_id => '1', :id => 'Test'}).gsub("\n", "")
1242 result = textilizable(raw, :edit_section_links => {:controller => 'wiki', :action => 'edit', :project_id => '1', :id => 'Test'}).gsub("\n", "")
1243
1243
1244 # heading that contains inline code
1244 # heading that contains inline code
1245 assert_match Regexp.new('<div class="contextual heading-2" title="Edit this section" id="section-4">' +
1245 assert_match Regexp.new('<div class="contextual heading-2" title="Edit this section" id="section-4">' +
1246 '<a class="icon-only icon-edit" href="/projects/1/wiki/Test/edit\?section=4">Edit this section</a></div>' +
1246 '<a class="icon-only icon-edit" href="/projects/1/wiki/Test/edit\?section=4">Edit this section</a></div>' +
1247 '<a name="Subtitle-with-inline-code"></a>' +
1247 '<a name="Subtitle-with-inline-code"></a>' +
1248 '<h2 >Subtitle with <code>inline code</code><a href="#Subtitle-with-inline-code" class="wiki-anchor">&para;</a></h2>'),
1248 '<h2 >Subtitle with <code>inline code</code><a href="#Subtitle-with-inline-code" class="wiki-anchor">&para;</a></h2>'),
1249 result
1249 result
1250
1250
1251 # last heading
1251 # last heading
1252 assert_match Regexp.new('<div class="contextual heading-2" title="Edit this section" id="section-5">' +
1252 assert_match Regexp.new('<div class="contextual heading-2" title="Edit this section" id="section-5">' +
1253 '<a class="icon-only icon-edit" href="/projects/1/wiki/Test/edit\?section=5">Edit this section</a></div>' +
1253 '<a class="icon-only icon-edit" href="/projects/1/wiki/Test/edit\?section=5">Edit this section</a></div>' +
1254 '<a name="Subtitle-after-pre-tag"></a>' +
1254 '<a name="Subtitle-after-pre-tag"></a>' +
1255 '<h2 >Subtitle after pre tag<a href="#Subtitle-after-pre-tag" class="wiki-anchor">&para;</a></h2>'),
1255 '<h2 >Subtitle after pre tag<a href="#Subtitle-after-pre-tag" class="wiki-anchor">&para;</a></h2>'),
1256 result
1256 result
1257 end
1257 end
1258
1258
1259 def test_default_formatter
1259 def test_default_formatter
1260 with_settings :text_formatting => 'unknown' do
1260 with_settings :text_formatting => 'unknown' do
1261 text = 'a *link*: http://www.example.net/'
1261 text = 'a *link*: http://www.example.net/'
1262 assert_equal '<p>a *link*: <a class="external" href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
1262 assert_equal '<p>a *link*: <a class="external" href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
1263 end
1263 end
1264 end
1264 end
1265
1265
1266 def test_parse_redmine_links_should_handle_a_tag_without_attributes
1266 def test_parse_redmine_links_should_handle_a_tag_without_attributes
1267 text = '<a>http://example.com</a>'
1267 text = '<a>http://example.com</a>'
1268 expected = text.dup
1268 expected = text.dup
1269 parse_redmine_links(text, nil, nil, nil, true, {})
1269 parse_redmine_links(text, nil, nil, nil, true, {})
1270 assert_equal expected, text
1270 assert_equal expected, text
1271 end
1271 end
1272
1272
1273 def test_due_date_distance_in_words
1273 def test_due_date_distance_in_words
1274 to_test = { Date.today => 'Due in 0 days',
1274 to_test = { Date.today => 'Due in 0 days',
1275 Date.today + 1 => 'Due in 1 day',
1275 Date.today + 1 => 'Due in 1 day',
1276 Date.today + 100 => 'Due in about 3 months',
1276 Date.today + 100 => 'Due in about 3 months',
1277 Date.today + 20000 => 'Due in over 54 years',
1277 Date.today + 20000 => 'Due in over 54 years',
1278 Date.today - 1 => '1 day late',
1278 Date.today - 1 => '1 day late',
1279 Date.today - 100 => 'about 3 months late',
1279 Date.today - 100 => 'about 3 months late',
1280 Date.today - 20000 => 'over 54 years late',
1280 Date.today - 20000 => 'over 54 years late',
1281 }
1281 }
1282 ::I18n.locale = :en
1282 ::I18n.locale = :en
1283 to_test.each do |date, expected|
1283 to_test.each do |date, expected|
1284 assert_equal expected, due_date_distance_in_words(date)
1284 assert_equal expected, due_date_distance_in_words(date)
1285 end
1285 end
1286 end
1286 end
1287
1287
1288 def test_avatar_enabled
1288 def test_avatar_enabled
1289 with_settings :gravatar_enabled => '1' do
1289 with_settings :gravatar_enabled => '1' do
1290 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1290 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1291 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1291 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1292 # Default size is 50
1292 # Default size is 50
1293 assert avatar('jsmith <jsmith@somenet.foo>').include?('size=50')
1293 assert avatar('jsmith <jsmith@somenet.foo>').include?('size=50')
1294 assert avatar('jsmith <jsmith@somenet.foo>', :size => 24).include?('size=24')
1294 assert avatar('jsmith <jsmith@somenet.foo>', :size => 24).include?('size=24')
1295 # Non-avatar options should be considered html options
1295 # Non-avatar options should be considered html options
1296 assert avatar('jsmith <jsmith@somenet.foo>', :title => 'John Smith').include?('title="John Smith"')
1296 assert avatar('jsmith <jsmith@somenet.foo>', :title => 'John Smith').include?('title="John Smith"')
1297 # The default class of the img tag should be gravatar
1297 # The default class of the img tag should be gravatar
1298 assert avatar('jsmith <jsmith@somenet.foo>').include?('class="gravatar"')
1298 assert avatar('jsmith <jsmith@somenet.foo>').include?('class="gravatar"')
1299 assert !avatar('jsmith <jsmith@somenet.foo>', :class => 'picture').include?('class="gravatar"')
1299 assert !avatar('jsmith <jsmith@somenet.foo>', :class => 'picture').include?('class="gravatar"')
1300 assert_nil avatar('jsmith')
1300 assert_nil avatar('jsmith')
1301 assert_nil avatar(nil)
1301 assert_nil avatar(nil)
1302 end
1302 end
1303 end
1303 end
1304
1304
1305 def test_avatar_disabled
1305 def test_avatar_disabled
1306 with_settings :gravatar_enabled => '0' do
1306 with_settings :gravatar_enabled => '0' do
1307 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
1307 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
1308 end
1308 end
1309 end
1309 end
1310
1310
1311 def test_link_to_user
1311 def test_link_to_user
1312 user = User.find(2)
1312 user = User.find(2)
1313 result = link_to("John Smith", "/users/2", :class => "user active")
1313 result = link_to("John Smith", "/users/2", :class => "user active")
1314 assert_equal result, link_to_user(user)
1314 assert_equal result, link_to_user(user)
1315 end
1315 end
1316
1316
1317 def test_link_to_user_should_not_link_to_locked_user
1317 def test_link_to_user_should_not_link_to_locked_user
1318 with_current_user nil do
1318 with_current_user nil do
1319 user = User.find(5)
1319 user = User.find(5)
1320 assert user.locked?
1320 assert user.locked?
1321 assert_equal 'Dave2 Lopper2', link_to_user(user)
1321 assert_equal 'Dave2 Lopper2', link_to_user(user)
1322 end
1322 end
1323 end
1323 end
1324
1324
1325 def test_link_to_user_should_link_to_locked_user_if_current_user_is_admin
1325 def test_link_to_user_should_link_to_locked_user_if_current_user_is_admin
1326 with_current_user User.find(1) do
1326 with_current_user User.find(1) do
1327 user = User.find(5)
1327 user = User.find(5)
1328 assert user.locked?
1328 assert user.locked?
1329 result = link_to("Dave2 Lopper2", "/users/5", :class => "user locked")
1329 result = link_to("Dave2 Lopper2", "/users/5", :class => "user locked")
1330 assert_equal result, link_to_user(user)
1330 assert_equal result, link_to_user(user)
1331 end
1331 end
1332 end
1332 end
1333
1333
1334 def test_link_to_user_should_not_link_to_anonymous
1334 def test_link_to_user_should_not_link_to_anonymous
1335 user = User.anonymous
1335 user = User.anonymous
1336 assert user.anonymous?
1336 assert user.anonymous?
1337 t = link_to_user(user)
1337 t = link_to_user(user)
1338 assert_equal ::I18n.t(:label_user_anonymous), t
1338 assert_equal ::I18n.t(:label_user_anonymous), t
1339 end
1339 end
1340
1340
1341 def test_link_to_attachment
1341 def test_link_to_attachment
1342 a = Attachment.find(3)
1342 a = Attachment.find(3)
1343 assert_equal '<a href="/attachments/3/logo.gif">logo.gif</a>',
1343 assert_equal '<a href="/attachments/3/logo.gif">logo.gif</a>',
1344 link_to_attachment(a)
1344 link_to_attachment(a)
1345 assert_equal '<a href="/attachments/3/logo.gif">Text</a>',
1345 assert_equal '<a href="/attachments/3/logo.gif">Text</a>',
1346 link_to_attachment(a, :text => 'Text')
1346 link_to_attachment(a, :text => 'Text')
1347 result = link_to("logo.gif", "/attachments/3/logo.gif", :class => "foo")
1347 result = link_to("logo.gif", "/attachments/3/logo.gif", :class => "foo")
1348 assert_equal result,
1348 assert_equal result,
1349 link_to_attachment(a, :class => 'foo')
1349 link_to_attachment(a, :class => 'foo')
1350 assert_equal '<a href="/attachments/download/3/logo.gif">logo.gif</a>',
1350 assert_equal '<a href="/attachments/download/3/logo.gif">logo.gif</a>',
1351 link_to_attachment(a, :download => true)
1351 link_to_attachment(a, :download => true)
1352 assert_equal '<a href="http://test.host/attachments/3/logo.gif">logo.gif</a>',
1352 assert_equal '<a href="http://test.host/attachments/3/logo.gif">logo.gif</a>',
1353 link_to_attachment(a, :only_path => false)
1353 link_to_attachment(a, :only_path => false)
1354 end
1354 end
1355
1355
1356 def test_thumbnail_tag
1356 def test_thumbnail_tag
1357 a = Attachment.find(3)
1357 a = Attachment.find(3)
1358 assert_select_in thumbnail_tag(a),
1358 assert_select_in thumbnail_tag(a),
1359 'a[href=?][title=?] img[alt="3"][src=?]',
1359 'a[href=?][title=?] img[alt="3"][src=?]',
1360 "/attachments/3/logo.gif", "logo.gif", "/attachments/thumbnail/3"
1360 "/attachments/3/logo.gif", "logo.gif", "/attachments/thumbnail/3"
1361 end
1361 end
1362
1362
1363 def test_link_to_project
1363 def test_link_to_project
1364 project = Project.find(1)
1364 project = Project.find(1)
1365 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
1365 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
1366 link_to_project(project)
1366 link_to_project(project)
1367 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
1367 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
1368 link_to_project(project, {:only_path => false, :jump => 'blah'})
1368 link_to_project(project, {:only_path => false, :jump => 'blah'})
1369 end
1369 end
1370
1370
1371 def test_link_to_project_settings
1371 def test_link_to_project_settings
1372 project = Project.find(1)
1372 project = Project.find(1)
1373 assert_equal '<a href="/projects/ecookbook/settings">eCookbook</a>', link_to_project_settings(project)
1373 assert_equal '<a href="/projects/ecookbook/settings">eCookbook</a>', link_to_project_settings(project)
1374
1374
1375 project.status = Project::STATUS_CLOSED
1375 project.status = Project::STATUS_CLOSED
1376 assert_equal '<a href="/projects/ecookbook">eCookbook</a>', link_to_project_settings(project)
1376 assert_equal '<a href="/projects/ecookbook">eCookbook</a>', link_to_project_settings(project)
1377
1377
1378 project.status = Project::STATUS_ARCHIVED
1378 project.status = Project::STATUS_ARCHIVED
1379 assert_equal 'eCookbook', link_to_project_settings(project)
1379 assert_equal 'eCookbook', link_to_project_settings(project)
1380 end
1380 end
1381
1381
1382 def test_link_to_legacy_project_with_numerical_identifier_should_use_id
1382 def test_link_to_legacy_project_with_numerical_identifier_should_use_id
1383 # numeric identifier are no longer allowed
1383 # numeric identifier are no longer allowed
1384 Project.where(:id => 1).update_all(:identifier => 25)
1384 Project.where(:id => 1).update_all(:identifier => 25)
1385 assert_equal '<a href="/projects/1">eCookbook</a>',
1385 assert_equal '<a href="/projects/1">eCookbook</a>',
1386 link_to_project(Project.find(1))
1386 link_to_project(Project.find(1))
1387 end
1387 end
1388
1388
1389 def test_principals_options_for_select_with_users
1389 def test_principals_options_for_select_with_users
1390 User.current = nil
1390 User.current = nil
1391 users = [User.find(2), User.find(4)]
1391 users = [User.find(2), User.find(4)]
1392 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
1392 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
1393 principals_options_for_select(users)
1393 principals_options_for_select(users)
1394 end
1394 end
1395
1395
1396 def test_principals_options_for_select_with_selected
1396 def test_principals_options_for_select_with_selected
1397 User.current = nil
1397 User.current = nil
1398 users = [User.find(2), User.find(4)]
1398 users = [User.find(2), User.find(4)]
1399 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
1399 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
1400 principals_options_for_select(users, User.find(4))
1400 principals_options_for_select(users, User.find(4))
1401 end
1401 end
1402
1402
1403 def test_principals_options_for_select_with_users_and_groups
1403 def test_principals_options_for_select_with_users_and_groups
1404 User.current = nil
1404 User.current = nil
1405 set_language_if_valid 'en'
1405 set_language_if_valid 'en'
1406 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
1406 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
1407 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
1407 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
1408 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
1408 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
1409 principals_options_for_select(users)
1409 principals_options_for_select(users)
1410 end
1410 end
1411
1411
1412 def test_principals_options_for_select_with_empty_collection
1412 def test_principals_options_for_select_with_empty_collection
1413 assert_equal '', principals_options_for_select([])
1413 assert_equal '', principals_options_for_select([])
1414 end
1414 end
1415
1415
1416 def test_principals_options_for_select_should_include_me_option_when_current_user_is_in_collection
1416 def test_principals_options_for_select_should_include_me_option_when_current_user_is_in_collection
1417 set_language_if_valid 'en'
1417 set_language_if_valid 'en'
1418 users = [User.find(2), User.find(4)]
1418 users = [User.find(2), User.find(4)]
1419 User.current = User.find(4)
1419 User.current = User.find(4)
1420 assert_include '<option value="4">&lt;&lt; me &gt;&gt;</option>', principals_options_for_select(users)
1420 assert_include '<option value="4">&lt;&lt; me &gt;&gt;</option>', principals_options_for_select(users)
1421 end
1421 end
1422
1422
1423 def test_stylesheet_link_tag_should_pick_the_default_stylesheet
1423 def test_stylesheet_link_tag_should_pick_the_default_stylesheet
1424 assert_match 'href="/stylesheets/styles.css"', stylesheet_link_tag("styles")
1424 assert_match 'href="/stylesheets/styles.css"', stylesheet_link_tag("styles")
1425 end
1425 end
1426
1426
1427 def test_stylesheet_link_tag_for_plugin_should_pick_the_plugin_stylesheet
1427 def test_stylesheet_link_tag_for_plugin_should_pick_the_plugin_stylesheet
1428 assert_match 'href="/plugin_assets/foo/stylesheets/styles.css"', stylesheet_link_tag("styles", :plugin => :foo)
1428 assert_match 'href="/plugin_assets/foo/stylesheets/styles.css"', stylesheet_link_tag("styles", :plugin => :foo)
1429 end
1429 end
1430
1430
1431 def test_image_tag_should_pick_the_default_image
1431 def test_image_tag_should_pick_the_default_image
1432 assert_match 'src="/images/image.png"', image_tag("image.png")
1432 assert_match 'src="/images/image.png"', image_tag("image.png")
1433 end
1433 end
1434
1434
1435 def test_image_tag_should_pick_the_theme_image_if_it_exists
1435 def test_image_tag_should_pick_the_theme_image_if_it_exists
1436 theme = Redmine::Themes.themes.last
1436 theme = Redmine::Themes.themes.last
1437 theme.images << 'image.png'
1437 theme.images << 'image.png'
1438
1438
1439 with_settings :ui_theme => theme.id do
1439 with_settings :ui_theme => theme.id do
1440 assert_match %|src="/themes/#{theme.dir}/images/image.png"|, image_tag("image.png")
1440 assert_match %|src="/themes/#{theme.dir}/images/image.png"|, image_tag("image.png")
1441 assert_match %|src="/images/other.png"|, image_tag("other.png")
1441 assert_match %|src="/images/other.png"|, image_tag("other.png")
1442 end
1442 end
1443 ensure
1443 ensure
1444 theme.images.delete 'image.png'
1444 theme.images.delete 'image.png'
1445 end
1445 end
1446
1446
1447 def test_image_tag_sfor_plugin_should_pick_the_plugin_image
1447 def test_image_tag_sfor_plugin_should_pick_the_plugin_image
1448 assert_match 'src="/plugin_assets/foo/images/image.png"', image_tag("image.png", :plugin => :foo)
1448 assert_match 'src="/plugin_assets/foo/images/image.png"', image_tag("image.png", :plugin => :foo)
1449 end
1449 end
1450
1450
1451 def test_javascript_include_tag_should_pick_the_default_javascript
1451 def test_javascript_include_tag_should_pick_the_default_javascript
1452 assert_match 'src="/javascripts/scripts.js"', javascript_include_tag("scripts")
1452 assert_match 'src="/javascripts/scripts.js"', javascript_include_tag("scripts")
1453 end
1453 end
1454
1454
1455 def test_javascript_include_tag_for_plugin_should_pick_the_plugin_javascript
1455 def test_javascript_include_tag_for_plugin_should_pick_the_plugin_javascript
1456 assert_match 'src="/plugin_assets/foo/javascripts/scripts.js"', javascript_include_tag("scripts", :plugin => :foo)
1456 assert_match 'src="/plugin_assets/foo/javascripts/scripts.js"', javascript_include_tag("scripts", :plugin => :foo)
1457 end
1457 end
1458
1458
1459 def test_raw_json_should_escape_closing_tags
1459 def test_raw_json_should_escape_closing_tags
1460 s = raw_json(["<foo>bar</foo>"])
1460 s = raw_json(["<foo>bar</foo>"])
1461 assert_include '\/foo', s
1461 assert_include '\/foo', s
1462 end
1462 end
1463
1463
1464 def test_raw_json_should_be_html_safe
1464 def test_raw_json_should_be_html_safe
1465 s = raw_json(["foo"])
1465 s = raw_json(["foo"])
1466 assert s.html_safe?
1466 assert s.html_safe?
1467 end
1467 end
1468
1468
1469 def test_html_title_should_app_title_if_not_set
1469 def test_html_title_should_app_title_if_not_set
1470 assert_equal 'Redmine', html_title
1470 assert_equal 'Redmine', html_title
1471 end
1471 end
1472
1472
1473 def test_html_title_should_join_items
1473 def test_html_title_should_join_items
1474 html_title 'Foo', 'Bar'
1474 html_title 'Foo', 'Bar'
1475 assert_equal 'Foo - Bar - Redmine', html_title
1475 assert_equal 'Foo - Bar - Redmine', html_title
1476 end
1476 end
1477
1477
1478 def test_html_title_should_append_current_project_name
1478 def test_html_title_should_append_current_project_name
1479 @project = Project.find(1)
1479 @project = Project.find(1)
1480 html_title 'Foo', 'Bar'
1480 html_title 'Foo', 'Bar'
1481 assert_equal 'Foo - Bar - eCookbook - Redmine', html_title
1481 assert_equal 'Foo - Bar - eCookbook - Redmine', html_title
1482 end
1482 end
1483
1483
1484 def test_title_should_return_a_h2_tag
1484 def test_title_should_return_a_h2_tag
1485 assert_equal '<h2>Foo</h2>', title('Foo')
1485 assert_equal '<h2>Foo</h2>', title('Foo')
1486 end
1486 end
1487
1487
1488 def test_title_should_set_html_title
1488 def test_title_should_set_html_title
1489 title('Foo')
1489 title('Foo')
1490 assert_equal 'Foo - Redmine', html_title
1490 assert_equal 'Foo - Redmine', html_title
1491 end
1491 end
1492
1492
1493 def test_title_should_turn_arrays_into_links
1493 def test_title_should_turn_arrays_into_links
1494 assert_equal '<h2><a href="/foo">Foo</a></h2>', title(['Foo', '/foo'])
1494 assert_equal '<h2><a href="/foo">Foo</a></h2>', title(['Foo', '/foo'])
1495 assert_equal 'Foo - Redmine', html_title
1495 assert_equal 'Foo - Redmine', html_title
1496 end
1496 end
1497
1497
1498 def test_title_should_join_items
1498 def test_title_should_join_items
1499 assert_equal '<h2>Foo &#187; Bar</h2>', title('Foo', 'Bar')
1499 assert_equal '<h2>Foo &#187; Bar</h2>', title('Foo', 'Bar')
1500 assert_equal 'Bar - Foo - Redmine', html_title
1500 assert_equal 'Bar - Foo - Redmine', html_title
1501 end
1501 end
1502
1502
1503 def test_favicon_path
1503 def test_favicon_path
1504 assert_match %r{^/favicon\.ico}, favicon_path
1504 assert_match %r{^/favicon\.ico}, favicon_path
1505 end
1505 end
1506
1506
1507 def test_favicon_path_with_suburi
1507 def test_favicon_path_with_suburi
1508 Redmine::Utils.relative_url_root = '/foo'
1508 Redmine::Utils.relative_url_root = '/foo'
1509 assert_match %r{^/foo/favicon\.ico}, favicon_path
1509 assert_match %r{^/foo/favicon\.ico}, favicon_path
1510 ensure
1510 ensure
1511 Redmine::Utils.relative_url_root = ''
1511 Redmine::Utils.relative_url_root = ''
1512 end
1512 end
1513
1513
1514 def test_favicon_url
1514 def test_favicon_url
1515 assert_match %r{^http://test\.host/favicon\.ico}, favicon_url
1515 assert_match %r{^http://test\.host/favicon\.ico}, favicon_url
1516 end
1516 end
1517
1517
1518 def test_favicon_url_with_suburi
1518 def test_favicon_url_with_suburi
1519 Redmine::Utils.relative_url_root = '/foo'
1519 Redmine::Utils.relative_url_root = '/foo'
1520 assert_match %r{^http://test\.host/foo/favicon\.ico}, favicon_url
1520 assert_match %r{^http://test\.host/foo/favicon\.ico}, favicon_url
1521 ensure
1521 ensure
1522 Redmine::Utils.relative_url_root = ''
1522 Redmine::Utils.relative_url_root = ''
1523 end
1523 end
1524
1524
1525 def test_truncate_single_line
1525 def test_truncate_single_line
1526 str = "01234"
1526 str = "01234"
1527 result = truncate_single_line_raw("#{str}\n#{str}", 10)
1527 result = truncate_single_line_raw("#{str}\n#{str}", 10)
1528 assert_equal "01234 0...", result
1528 assert_equal "01234 0...", result
1529 assert !result.html_safe?
1529 assert !result.html_safe?
1530 result = truncate_single_line_raw("#{str}<&#>\n#{str}#{str}", 16)
1530 result = truncate_single_line_raw("#{str}<&#>\n#{str}#{str}", 16)
1531 assert_equal "01234<&#> 012...", result
1531 assert_equal "01234<&#> 012...", result
1532 assert !result.html_safe?
1532 assert !result.html_safe?
1533 end
1533 end
1534
1534
1535 def test_truncate_single_line_non_ascii
1535 def test_truncate_single_line_non_ascii
1536 ja = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e".force_encoding('UTF-8')
1536 ja = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e".force_encoding('UTF-8')
1537 result = truncate_single_line_raw("#{ja}\n#{ja}\n#{ja}", 10)
1537 result = truncate_single_line_raw("#{ja}\n#{ja}\n#{ja}", 10)
1538 assert_equal "#{ja} #{ja}...", result
1538 assert_equal "#{ja} #{ja}...", result
1539 assert !result.html_safe?
1539 assert !result.html_safe?
1540 end
1540 end
1541 end
1541 end
@@ -1,62 +1,77
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
2 # Copyright (C) 2006-2016 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../../../../test_helper', __FILE__)
18 require File.expand_path('../../../../../test_helper', __FILE__)
19
19
20 class Redmine::FieldFormatTest < ActionView::TestCase
20 class Redmine::FieldFormatTest < ActionView::TestCase
21 include ApplicationHelper
21 include ApplicationHelper
22
22
23 def setup
24 set_language_if_valid 'en'
25 end
26
23 def test_string_field_with_text_formatting_disabled_should_not_format_text
27 def test_string_field_with_text_formatting_disabled_should_not_format_text
24 field = IssueCustomField.new(:field_format => 'string')
28 field = IssueCustomField.new(:field_format => 'string')
25 custom_value = CustomValue.new(:custom_field => field, :customized => Issue.new, :value => "*foo*")
29 custom_value = CustomValue.new(:custom_field => field, :customized => Issue.new, :value => "*foo*")
26
30
27 assert_equal "*foo*", field.format.formatted_custom_value(self, custom_value, false)
31 assert_equal "*foo*", field.format.formatted_custom_value(self, custom_value, false)
28 assert_equal "*foo*", field.format.formatted_custom_value(self, custom_value, true)
32 assert_equal "*foo*", field.format.formatted_custom_value(self, custom_value, true)
29 end
33 end
30
34
31 def test_string_field_with_text_formatting_enabled_should_format_text
35 def test_string_field_with_text_formatting_enabled_should_format_text
32 field = IssueCustomField.new(:field_format => 'string', :text_formatting => 'full')
36 field = IssueCustomField.new(:field_format => 'string', :text_formatting => 'full')
33 custom_value = CustomValue.new(:custom_field => field, :customized => Issue.new, :value => "*foo*")
37 custom_value = CustomValue.new(:custom_field => field, :customized => Issue.new, :value => "*foo*")
34
38
35 assert_equal "*foo*", field.format.formatted_custom_value(self, custom_value, false)
39 assert_equal "*foo*", field.format.formatted_custom_value(self, custom_value, false)
36 assert_include "<strong>foo</strong>", field.format.formatted_custom_value(self, custom_value, true)
40 assert_include "<strong>foo</strong>", field.format.formatted_custom_value(self, custom_value, true)
37 end
41 end
38
42
39 def test_text_field_with_text_formatting_disabled_should_not_format_text
43 def test_text_field_with_text_formatting_disabled_should_not_format_text
40 field = IssueCustomField.new(:field_format => 'text')
44 field = IssueCustomField.new(:field_format => 'text')
41 custom_value = CustomValue.new(:custom_field => field, :customized => Issue.new, :value => "*foo*\nbar")
45 custom_value = CustomValue.new(:custom_field => field, :customized => Issue.new, :value => "*foo*\nbar")
42
46
43 assert_equal "*foo*\nbar", field.format.formatted_custom_value(self, custom_value, false)
47 assert_equal "*foo*\nbar", field.format.formatted_custom_value(self, custom_value, false)
44 assert_include "*foo*\n<br />bar", field.format.formatted_custom_value(self, custom_value, true)
48 assert_include "*foo*\n<br />bar", field.format.formatted_custom_value(self, custom_value, true)
45 end
49 end
46
50
47 def test_text_field_with_text_formatting_enabled_should_format_text
51 def test_text_field_with_text_formatting_enabled_should_format_text
48 field = IssueCustomField.new(:field_format => 'text', :text_formatting => 'full')
52 field = IssueCustomField.new(:field_format => 'text', :text_formatting => 'full')
49 custom_value = CustomValue.new(:custom_field => field, :customized => Issue.new, :value => "*foo*\nbar")
53 custom_value = CustomValue.new(:custom_field => field, :customized => Issue.new, :value => "*foo*\nbar")
50
54
51 assert_equal "*foo*\nbar", field.format.formatted_custom_value(self, custom_value, false)
55 assert_equal "*foo*\nbar", field.format.formatted_custom_value(self, custom_value, false)
52 assert_include "<strong>foo</strong>", field.format.formatted_custom_value(self, custom_value, true)
56 assert_include "<strong>foo</strong>", field.format.formatted_custom_value(self, custom_value, true)
53 end
57 end
54
58
59 def test_should_validate_url_pattern_with_safe_scheme
60 field = IssueCustomField.new(:field_format => 'string', :name => 'URL', :url_pattern => 'http://foo/%value%')
61 assert_save field
62 end
63
64 def test_should_not_validate_url_pattern_with_unsafe_scheme
65 field = IssueCustomField.new(:field_format => 'string', :name => 'URL', :url_pattern => 'foo://foo/%value%')
66 assert !field.save
67 assert_include "URL is invalid", field.errors.full_messages
68 end
69
55 def test_text_field_with_url_pattern_should_format_as_link
70 def test_text_field_with_url_pattern_should_format_as_link
56 field = IssueCustomField.new(:field_format => 'string', :url_pattern => 'http://foo/%value%')
71 field = IssueCustomField.new(:field_format => 'string', :url_pattern => 'http://foo/%value%')
57 custom_value = CustomValue.new(:custom_field => field, :customized => Issue.new, :value => "bar")
72 custom_value = CustomValue.new(:custom_field => field, :customized => Issue.new, :value => "bar")
58
73
59 assert_equal "bar", field.format.formatted_custom_value(self, custom_value, false)
74 assert_equal "bar", field.format.formatted_custom_value(self, custom_value, false)
60 assert_equal '<a href="http://foo/bar">bar</a>', field.format.formatted_custom_value(self, custom_value, true)
75 assert_equal '<a href="http://foo/bar">bar</a>', field.format.formatted_custom_value(self, custom_value, true)
61 end
76 end
62 end
77 end
General Comments 0
You need to be logged in to leave comments. Login now