##// END OF EJS Templates
Merged r2664, r2665, r2670, r2674, r2677, r2679 from trunk....
Jean-Philippe Lang -
r2589:7b699af83748
parent child
Show More
@@ -1,151 +1,150
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require "digest/md5"
19 19
20 20 class Attachment < ActiveRecord::Base
21 21 belongs_to :container, :polymorphic => true
22 22 belongs_to :author, :class_name => "User", :foreign_key => "author_id"
23 23
24 24 validates_presence_of :container, :filename, :author
25 25 validates_length_of :filename, :maximum => 255
26 26 validates_length_of :disk_filename, :maximum => 255
27 27
28 28 acts_as_event :title => :filename,
29 29 :url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
30 30
31 31 acts_as_activity_provider :type => 'files',
32 32 :permission => :view_files,
33 33 :author_key => :author_id,
34 34 :find_options => {:select => "#{Attachment.table_name}.*",
35 35 :joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
36 36 "LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )"}
37 37
38 38 acts_as_activity_provider :type => 'documents',
39 39 :permission => :view_documents,
40 40 :author_key => :author_id,
41 41 :find_options => {:select => "#{Attachment.table_name}.*",
42 42 :joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
43 43 "LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
44 44
45 45 cattr_accessor :storage_path
46 46 @@storage_path = "#{RAILS_ROOT}/files"
47 47
48 48 def validate
49 49 errors.add_to_base :too_long if self.filesize > Setting.attachment_max_size.to_i.kilobytes
50 50 end
51 51
52 52 def file=(incoming_file)
53 53 unless incoming_file.nil?
54 54 @temp_file = incoming_file
55 55 if @temp_file.size > 0
56 56 self.filename = sanitize_filename(@temp_file.original_filename)
57 57 self.disk_filename = Attachment.disk_filename(filename)
58 58 self.content_type = @temp_file.content_type.to_s.chomp
59 59 self.filesize = @temp_file.size
60 60 end
61 61 end
62 62 end
63 63
64 64 def file
65 65 nil
66 66 end
67 67
68 # Copy temp file to its final location
68 # Copies the temporary file to its final location
69 # and computes its MD5 hash
69 70 def before_save
70 71 if @temp_file && (@temp_file.size > 0)
71 72 logger.debug("saving '#{self.diskfile}'")
73 md5 = Digest::MD5.new
72 74 File.open(diskfile, "wb") do |f|
73 f.write(@temp_file.read)
75 buffer = ""
76 while (buffer = @temp_file.read(8192))
77 f.write(buffer)
78 md5.update(buffer)
79 end
74 80 end
75 self.digest = self.class.digest(diskfile)
81 self.digest = md5.hexdigest
76 82 end
77 83 # Don't save the content type if it's longer than the authorized length
78 84 if self.content_type && self.content_type.length > 255
79 85 self.content_type = nil
80 86 end
81 87 end
82 88
83 89 # Deletes file on the disk
84 90 def after_destroy
85 91 File.delete(diskfile) if !filename.blank? && File.exist?(diskfile)
86 92 end
87 93
88 94 # Returns file's location on disk
89 95 def diskfile
90 96 "#{@@storage_path}/#{self.disk_filename}"
91 97 end
92 98
93 99 def increment_download
94 100 increment!(:downloads)
95 101 end
96 102
97 103 def project
98 104 container.project
99 105 end
100 106
101 107 def visible?(user=User.current)
102 108 container.attachments_visible?(user)
103 109 end
104 110
105 111 def deletable?(user=User.current)
106 112 container.attachments_deletable?(user)
107 113 end
108 114
109 115 def image?
110 116 self.filename =~ /\.(jpe?g|gif|png)$/i
111 117 end
112 118
113 119 def is_text?
114 120 Redmine::MimeType.is_type?('text', filename)
115 121 end
116 122
117 123 def is_diff?
118 124 self.filename =~ /\.(patch|diff)$/i
119 125 end
120 126
121 127 private
122 128 def sanitize_filename(value)
123 129 # get only the filename, not the whole path
124 130 just_filename = value.gsub(/^.*(\\|\/)/, '')
125 131 # NOTE: File.basename doesn't work right with Windows paths on Unix
126 132 # INCORRECT: just_filename = File.basename(value.gsub('\\\\', '/'))
127 133
128 134 # Finally, replace all non alphanumeric, hyphens or periods with underscore
129 135 @filename = just_filename.gsub(/[^\w\.\-]/,'_')
130 136 end
131 137
132 138 # Returns an ASCII or hashed filename
133 139 def self.disk_filename(filename)
134 140 df = DateTime.now.strftime("%y%m%d%H%M%S") + "_"
135 141 if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
136 142 df << filename
137 143 else
138 144 df << Digest::MD5.hexdigest(filename)
139 145 # keep the extension if any
140 146 df << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
141 147 end
142 148 df
143 149 end
144
145 # Returns the MD5 digest of the file at given path
146 def self.digest(filename)
147 File.open(filename, 'rb') do |f|
148 Digest::MD5.hexdigest(f.read)
149 end
150 end
151 150 end
@@ -1,24 +1,29
1 1 <h2><%=l(:label_settings)%></h2>
2 2
3 3 <% tabs = project_settings_tabs %>
4
5 <% if tabs.any? %>
4 6 <% selected_tab = params[:tab] ? params[:tab].to_s : tabs.first[:name] %>
5 7
6 8 <div class="tabs">
7 9 <ul>
8 10 <% tabs.each do |tab| -%>
9 11 <li><%= link_to l(tab[:label]), { :tab => tab[:name] },
10 12 :id => "tab-#{tab[:name]}",
11 13 :class => (tab[:name] != selected_tab ? nil : 'selected'),
12 14 :onclick => "showTab('#{tab[:name]}'); this.blur(); return false;" %></li>
13 15 <% end -%>
14 16 </ul>
15 17 </div>
16 18
17 19 <% tabs.each do |tab| -%>
18 20 <%= content_tag('div', render(:partial => tab[:partial]),
19 21 :id => "tab-content-#{tab[:name]}",
20 22 :style => (tab[:name] != selected_tab ? 'display:none' : nil),
21 23 :class => 'tab-content') %>
22 24 <% end -%>
25 <% else %>
26 <p class="nodata"><%= l(:label_no_data) %></p>
27 <% end %>
23 28
24 29 <% html_title(l(:label_settings)) -%>
@@ -1,896 +1,905
1 1 == Redmine changelog
2 2
3 3 Redmine - project management software
4 4 Copyright (C) 2006-2009 Jean-Philippe Lang
5 5 http://www.redmine.org/
6 6
7 7
8 == 2009-xx-xx v0.8.4
9
10 * Allow textile mailto links
11 * Fixed: memory consumption when uploading file
12 * Fixed: Mercurial integration doesn't work if Redmine is installed in folder path containing space
13 * Fixed: an error is raised when no tab is available on project settings
14 * Fixed: insert image macro corrupts urls with excalamation marks
15
16
8 17 == 2009-04-05 v0.8.3
9 18
10 19 * Separate project field and subject in cross-project issue view
11 20 * Ability to set language for redmine:load_default_data task using REDMINE_LANG environment variable
12 21 * Rescue Redmine::DefaultData::DataAlreadyLoaded in redmine:load_default_data task
13 22 * CSS classes to highlight own and assigned issues
14 23 * Hide "New file" link on wiki pages from printing
15 24 * Flush buffer when asking for language in redmine:load_default_data task
16 25 * Minimum project identifier length set to 1
17 26 * Include headers so that emails don't trigger vacation auto-responders
18 27 * Fixed: Time entries csv export links for all projects are malformed
19 28 * Fixed: Files without Version aren't visible in the Activity page
20 29 * Fixed: Commit logs are centered in the repo browser
21 30 * Fixed: News summary field content is not searchable
22 31 * Fixed: Journal#save has a wrong signature
23 32 * Fixed: Email footer signature convention
24 33 * Fixed: Timelog report do not show time for non-versioned issues
25 34
26 35
27 36 == 2009-03-07 v0.8.2
28 37
29 38 * Send an email to the user when an administrator activates a registered user
30 39 * Strip keywords from received email body
31 40 * Footer updated to 2009
32 41 * Show RSS-link even when no issues is found
33 42 * One click filter action in activity view
34 43 * Clickable/linkable line #'s while browsing the repo or viewing a file
35 44 * Links to versions on files list
36 45 * Added request and controller objects to the hooks by default
37 46 * Fixed: exporting an issue with attachments to PDF raises an error
38 47 * Fixed: "too few arguments" error may occur on activerecord error translation
39 48 * Fixed: "Default columns Displayed on the Issues list" setting is not easy to read
40 49 * Fixed: visited links to closed tickets are not striked through with IE6
41 50 * Fixed: MailHandler#plain_text_body returns nil if there was nothing to strip
42 51 * Fixed: MailHandler raises an error when processing an email without From header
43 52
44 53
45 54 == 2009-02-15 v0.8.1
46 55
47 56 * Select watchers on new issue form
48 57 * Issue description is no longer a required field
49 58 * Files module: ability to add files without version
50 59 * Jump to the current tab when using the project quick-jump combo
51 60 * Display a warning if some attachments were not saved
52 61 * Import custom fields values from emails on issue creation
53 62 * Show view/annotate/download links on entry and annotate views
54 63 * Admin Info Screen: Display if plugin assets directory is writable
55 64 * Adds a 'Create and continue' button on the new issue form
56 65 * IMAP: add options to move received emails
57 66 * Do not show Category field when categories are not defined
58 67 * Lower the project identifier limit to a minimum of two characters
59 68 * Add "closed" html class to closed entries in issue list
60 69 * Fixed: broken redirect URL on login failure
61 70 * Fixed: Deleted files are shown when using Darcs
62 71 * Fixed: Darcs adapter works on Win32 only
63 72 * Fixed: syntax highlight doesn't appear in new ticket preview
64 73 * Fixed: email notification for changes I make still occurs when running Repository.fetch_changesets
65 74 * Fixed: no error is raised when entering invalid hours on the issue update form
66 75 * Fixed: Details time log report CSV export doesn't honour date format from settings
67 76 * Fixed: invalid css classes on issue details
68 77 * Fixed: Trac importer creates duplicate custom values
69 78 * Fixed: inline attached image should not match partial filename
70 79
71 80
72 81 == 2008-12-30 v0.8.0
73 82
74 83 * Setting added in order to limit the number of diff lines that should be displayed
75 84 * Makes logged-in username in topbar linking to
76 85 * Mail handler: strip tags when receiving a html-only email
77 86 * Mail handler: add watchers before sending notification
78 87 * Adds a css class (overdue) to overdue issues on issue lists and detail views
79 88 * Fixed: project activity truncated after viewing user's activity
80 89 * Fixed: email address entered for password recovery shouldn't be case-sensitive
81 90 * Fixed: default flag removed when editing a default enumeration
82 91 * Fixed: default category ignored when adding a document
83 92 * Fixed: error on repository user mapping when a repository username is blank
84 93 * Fixed: Firefox cuts off large diffs
85 94 * Fixed: CVS browser should not show dead revisions (deleted files)
86 95 * Fixed: escape double-quotes in image titles
87 96 * Fixed: escape textarea content when editing a issue note
88 97 * Fixed: JS error on context menu with IE
89 98 * Fixed: bold syntax around single character in series doesn't work
90 99 * Fixed several XSS vulnerabilities
91 100 * Fixed a SQL injection vulnerability
92 101
93 102
94 103 == 2008-12-07 v0.8.0-rc1
95 104
96 105 * Wiki page protection
97 106 * Wiki page hierarchy. Parent page can be assigned on the Rename screen
98 107 * Adds support for issue creation via email
99 108 * Adds support for free ticket filtering and custom queries on Gantt chart and calendar
100 109 * Cross-project search
101 110 * Ability to search a project and its subprojects
102 111 * Ability to search the projects the user belongs to
103 112 * Adds custom fields on time entries
104 113 * Adds boolean and list custom fields for time entries as criteria on time report
105 114 * Cross-project time reports
106 115 * Display latest user's activity on account/show view
107 116 * Show last connexion time on user's page
108 117 * Obfuscates email address on user's account page using javascript
109 118 * wiki TOC rendered as an unordered list
110 119 * Adds the ability to search for a user on the administration users list
111 120 * Adds the ability to search for a project name or identifier on the administration projects list
112 121 * Redirect user to the previous page after logging in
113 122 * Adds a permission 'view wiki edits' so that wiki history can be hidden to certain users
114 123 * Adds permissions for viewing the watcher list and adding new watchers on the issue detail view
115 124 * Adds permissions to let users edit and/or delete their messages
116 125 * Link to activity view when displaying dates
117 126 * Hide Redmine version in atom feeds and pdf properties
118 127 * Maps repository users to Redmine users. Users with same username or email are automatically mapped. Mapping can be manually adjusted in repository settings. Multiple usernames can be mapped to the same Redmine user.
119 128 * Sort users by their display names so that user dropdown lists are sorted alphabetically
120 129 * Adds estimated hours to issue filters
121 130 * Switch order of current and previous revisions in side-by-side diff
122 131 * Render the commit changes list as a tree
123 132 * Adds watch/unwatch functionality at forum topic level
124 133 * When moving an issue to another project, reassign it to the category with same name if any
125 134 * Adds child_pages macro for wiki pages
126 135 * Use GET instead of POST on roadmap (#718), gantt and calendar forms
127 136 * Search engine: display total results count and count by result type
128 137 * Email delivery configuration moved to an unversioned YAML file (config/email.yml, see the sample file)
129 138 * Adds icons on search results
130 139 * Adds 'Edit' link on account/show for admin users
131 140 * Adds Lock/Unlock/Activate link on user edit screen
132 141 * Adds user count in status drop down on admin user list
133 142 * Adds multi-levels blockquotes support by using > at the beginning of lines
134 143 * Adds a Reply link to each issue note
135 144 * Adds plain text only option for mail notifications
136 145 * Gravatar support for issue detail, user grid, and activity stream (disabled by default)
137 146 * Adds 'Delete wiki pages attachments' permission
138 147 * Show the most recent file when displaying an inline image
139 148 * Makes permission screens localized
140 149 * AuthSource list: display associated users count and disable 'Delete' buton if any
141 150 * Make the 'duplicates of' relation asymmetric
142 151 * Adds username to the password reminder email
143 152 * Adds links to forum messages using message#id syntax
144 153 * Allow same name for custom fields on different object types
145 154 * One-click bulk edition using the issue list context menu within the same project
146 155 * Adds support for commit logs reencoding to UTF-8 before insertion in the database. Source encoding of commit logs can be selected in Application settings -> Repositories.
147 156 * Adds checkboxes toggle links on permissions report
148 157 * Adds Trac-Like anchors on wiki headings
149 158 * Adds support for wiki links with anchor
150 159 * Adds category to the issue context menu
151 160 * Adds a workflow overview screen
152 161 * Appends the filename to the attachment url so that clients that ignore content-disposition http header get the real filename
153 162 * Dots allowed in custom field name
154 163 * Adds posts quoting functionality
155 164 * Adds an option to generate sequential project identifiers
156 165 * Adds mailto link on the user administration list
157 166 * Ability to remove enumerations (activities, priorities, document categories) that are in use. Associated objects can be reassigned to another value
158 167 * Gantt chart: display issues that don't have a due date if they are assigned to a version with a date
159 168 * Change projects homepage limit to 255 chars
160 169 * Improved on-the-fly account creation. If some attributes are missing (eg. not present in the LDAP) or are invalid, the registration form is displayed so that the user is able to fill or fix these attributes
161 170 * Adds "please select" to activity select box if no activity is set as default
162 171 * Do not silently ignore timelog validation failure on issue edit
163 172 * Adds a rake task to send reminder emails
164 173 * Allow empty cells in wiki tables
165 174 * Makes wiki text formatter pluggable
166 175 * Adds back textile acronyms support
167 176 * Remove pre tag attributes
168 177 * Plugin hooks
169 178 * Pluggable admin menu
170 179 * Plugins can provide activity content
171 180 * Moves plugin list to its own administration menu item
172 181 * Adds url and author_url plugin attributes
173 182 * Adds Plugin#requires_redmine method so that plugin compatibility can be checked against current Redmine version
174 183 * Adds atom feed on time entries details
175 184 * Adds project name to issues feed title
176 185 * Adds a css class on menu items in order to apply item specific styles (eg. icons)
177 186 * Adds a Redmine plugin generators
178 187 * Adds timelog link to the issue context menu
179 188 * Adds links to the user page on various views
180 189 * Turkish translation by Ismail Sezen
181 190 * Catalan translation
182 191 * Vietnamese translation
183 192 * Slovak translation
184 193 * Better naming of activity feed if only one kind of event is displayed
185 194 * Enable syntax highlight on issues, messages and news
186 195 * Add target version to the issue list context menu
187 196 * Hide 'Target version' filter if no version is defined
188 197 * Add filters on cross-project issue list for custom fields marked as 'For all projects'
189 198 * Turn ftp urls into links
190 199 * Hiding the View Differences button when a wiki page's history only has one version
191 200 * Messages on a Board can now be sorted by the number of replies
192 201 * Adds a class ('me') to events of the activity view created by current user
193 202 * Strip pre/code tags content from activity view events
194 203 * Display issue notes in the activity view
195 204 * Adds links to changesets atom feed on repository browser
196 205 * Track project and tracker changes in issue history
197 206 * Adds anchor to atom feed messages links
198 207 * Adds a key in lang files to set the decimal separator (point or comma) in csv exports
199 208 * Makes importer work with Trac 0.8.x
200 209 * Upgraded to Prototype 1.6.0.1
201 210 * File viewer for attached text files
202 211 * Menu mapper: add support for :before, :after and :last options to #push method and add #delete method
203 212 * Removed inconsistent revision numbers on diff view
204 213 * CVS: add support for modules names with spaces
205 214 * Log the user in after registration if account activation is not needed
206 215 * Mercurial adapter improvements
207 216 * Trac importer: read session_attribute table to find user's email and real name
208 217 * Ability to disable unused SCM adapters in application settings
209 218 * Adds Filesystem adapter
210 219 * Clear changesets and changes with raw sql when deleting a repository for performance
211 220 * Redmine.pm now uses the 'commit access' permission defined in Redmine
212 221 * Reposman can create any type of scm (--scm option)
213 222 * Reposman creates a repository if the 'repository' module is enabled at project level only
214 223 * Display svn properties in the browser, svn >= 1.5.0 only
215 224 * Reduces memory usage when importing large git repositories
216 225 * Wider SVG graphs in repository stats
217 226 * SubversionAdapter#entries performance improvement
218 227 * SCM browser: ability to download raw unified diffs
219 228 * More detailed error message in log when scm command fails
220 229 * Adds support for file viewing with Darcs 2.0+
221 230 * Check that git changeset is not in the database before creating it
222 231 * Unified diff viewer for attached files with .patch or .diff extension
223 232 * File size display with Bazaar repositories
224 233 * Git adapter: use commit time instead of author time
225 234 * Prettier url for changesets
226 235 * Makes changes link to entries on the revision view
227 236 * Adds a field on the repository view to browse at specific revision
228 237 * Adds new projects atom feed
229 238 * Added rake tasks to generate rcov code coverage reports
230 239 * Add Redcloth's :block_markdown_rule to allow horizontal rules in wiki
231 240 * Show the project hierarchy in the drop down list for new membership on user administration screen
232 241 * Split user edit screen into tabs
233 242 * Renames bundled RedCloth to RedCloth3 to avoid RedCloth 4 to be loaded instead
234 243 * Fixed: Roadmap crashes when a version has a due date > 2037
235 244 * Fixed: invalid effective date (eg. 99999-01-01) causes an error on version edition screen
236 245 * Fixed: login filter providing incorrect back_url for Redmine installed in sub-directory
237 246 * Fixed: logtime entry duplicated when edited from parent project
238 247 * Fixed: wrong digest for text files under Windows
239 248 * Fixed: associated revisions are displayed in wrong order on issue view
240 249 * Fixed: Git Adapter date parsing ignores timezone
241 250 * Fixed: Printing long roadmap doesn't split across pages
242 251 * Fixes custom fields display order at several places
243 252 * Fixed: urls containing @ are parsed as email adress by the wiki formatter
244 253 * Fixed date filters accuracy with SQLite
245 254 * Fixed: tokens not escaped in highlight_tokens regexp
246 255 * Fixed Bazaar shared repository browsing
247 256 * Fixes platform determination under JRuby
248 257 * Fixed: Estimated time in issue's journal should be rounded to two decimals
249 258 * Fixed: 'search titles only' box ignored after one search is done on titles only
250 259 * Fixed: non-ASCII subversion path can't be displayed
251 260 * Fixed: Inline images don't work if file name has upper case letters or if image is in BMP format
252 261 * Fixed: document listing shows on "my page" when viewing documents is disabled for the role
253 262 * Fixed: Latest news appear on the homepage for projects with the News module disabled
254 263 * Fixed: cross-project issue list should not show issues of projects for which the issue tracking module was disabled
255 264 * Fixed: the default status is lost when reordering issue statuses
256 265 * Fixes error with Postgresql and non-UTF8 commit logs
257 266 * Fixed: textile footnotes no longer work
258 267 * Fixed: http links containing parentheses fail to reder correctly
259 268 * Fixed: GitAdapter#get_rev should use current branch instead of hardwiring master
260 269
261 270
262 271 == 2008-07-06 v0.7.3
263 272
264 273 * Allow dot in firstnames and lastnames
265 274 * Add project name to cross-project Atom feeds
266 275 * Encoding set to utf8 in example database.yml
267 276 * HTML titles on forums related views
268 277 * Fixed: various XSS vulnerabilities
269 278 * Fixed: Entourage (and some old client) fails to correctly render notification styles
270 279 * Fixed: Fixed: timelog redirects inappropriately when :back_url is blank
271 280 * Fixed: wrong relative paths to images in wiki_syntax.html
272 281
273 282
274 283 == 2008-06-15 v0.7.2
275 284
276 285 * "New Project" link on Projects page
277 286 * Links to repository directories on the repo browser
278 287 * Move status to front in Activity View
279 288 * Remove edit step from Status context menu
280 289 * Fixed: No way to do textile horizontal rule
281 290 * Fixed: Repository: View differences doesn't work
282 291 * Fixed: attachement's name maybe invalid.
283 292 * Fixed: Error when creating a new issue
284 293 * Fixed: NoMethodError on @available_filters.has_key?
285 294 * Fixed: Check All / Uncheck All in Email Settings
286 295 * Fixed: "View differences" of one file at /repositories/revision/ fails
287 296 * Fixed: Column width in "my page"
288 297 * Fixed: private subprojects are listed on Issues view
289 298 * Fixed: Textile: bold, italics, underline, etc... not working after parentheses
290 299 * Fixed: Update issue form: comment field from log time end out of screen
291 300 * Fixed: Editing role: "issue can be assigned to this role" out of box
292 301 * Fixed: Unable use angular braces after include word
293 302 * Fixed: Using '*' as keyword for repository referencing keywords doesn't work
294 303 * Fixed: Subversion repository "View differences" on each file rise ERROR
295 304 * Fixed: View differences for individual file of a changeset fails if the repository URL doesn't point to the repository root
296 305 * Fixed: It is possible to lock out the last admin account
297 306 * Fixed: Wikis are viewable for anonymous users on public projects, despite not granting access
298 307 * Fixed: Issue number display clipped on 'my issues'
299 308 * Fixed: Roadmap version list links not carrying state
300 309 * Fixed: Log Time fieldset in IssueController#edit doesn't set default Activity as default
301 310 * Fixed: git's "get_rev" API should use repo's current branch instead of hardwiring "master"
302 311 * Fixed: browser's language subcodes ignored
303 312 * Fixed: Error on project selection with numeric (only) identifier.
304 313 * Fixed: Link to PDF doesn't work after creating new issue
305 314 * Fixed: "Replies" should not be shown on forum threads that are locked
306 315 * Fixed: SVN errors lead to svn username/password being displayed to end users (security issue)
307 316 * Fixed: http links containing hashes don't display correct
308 317 * Fixed: Allow ampersands in Enumeration names
309 318 * Fixed: Atom link on saved query does not include query_id
310 319 * Fixed: Logtime info lost when there's an error updating an issue
311 320 * Fixed: TOC does not parse colorization markups
312 321 * Fixed: CVS: add support for modules names with spaces
313 322 * Fixed: Bad rendering on projects/add
314 323 * Fixed: exception when viewing differences on cvs
315 324 * Fixed: export issue to pdf will messup when use Chinese language
316 325 * Fixed: Redmine::Scm::Adapters::GitAdapter#get_rev ignored GIT_BIN constant
317 326 * Fixed: Adding non-ASCII new issue type in the New Issue page have encoding error using IE
318 327 * Fixed: Importing from trac : some wiki links are messed
319 328 * Fixed: Incorrect weekend definition in Hebrew calendar locale
320 329 * Fixed: Atom feeds don't provide author section for repository revisions
321 330 * Fixed: In Activity views, changesets titles can be multiline while they should not
322 331 * Fixed: Ignore unreadable subversion directories (read disabled using authz)
323 332 * Fixed: lib/SVG/Graph/Graph.rb can't externalize stylesheets
324 333 * Fixed: Close statement handler in Redmine.pm
325 334
326 335
327 336 == 2008-05-04 v0.7.1
328 337
329 338 * Thai translation added (Gampol Thitinilnithi)
330 339 * Translations updates
331 340 * Escape HTML comment tags
332 341 * Prevent "can't convert nil into String" error when :sort_order param is not present
333 342 * Fixed: Updating tickets add a time log with zero hours
334 343 * Fixed: private subprojects names are revealed on the project overview
335 344 * Fixed: Search for target version of "none" fails with postgres 8.3
336 345 * Fixed: Home, Logout, Login links shouldn't be absolute links
337 346 * Fixed: 'Latest projects' box on the welcome screen should be hidden if there are no projects
338 347 * Fixed: error when using upcase language name in coderay
339 348 * Fixed: error on Trac import when :due attribute is nil
340 349
341 350
342 351 == 2008-04-28 v0.7.0
343 352
344 353 * Forces Redmine to use rails 2.0.2 gem when vendor/rails is not present
345 354 * Queries can be marked as 'For all projects'. Such queries will be available on all projects and on the global issue list.
346 355 * Add predefined date ranges to the time report
347 356 * Time report can be done at issue level
348 357 * Various timelog report enhancements
349 358 * Accept the following formats for "hours" field: 1h, 1 h, 1 hour, 2 hours, 30m, 30min, 1h30, 1h30m, 1:30
350 359 * Display the context menu above and/or to the left of the click if needed
351 360 * Make the admin project files list sortable
352 361 * Mercurial: display working directory files sizes unless browsing a specific revision
353 362 * Preserve status filter and page number when using lock/unlock/activate links on the users list
354 363 * Redmine.pm support for LDAP authentication
355 364 * Better error message and AR errors in log for failed LDAP on-the-fly user creation
356 365 * Redirected user to where he is coming from after logging hours
357 366 * Warn user that subprojects are also deleted when deleting a project
358 367 * Include subprojects versions on calendar and gantt
359 368 * Notify project members when a message is posted if they want to receive notifications
360 369 * Fixed: Feed content limit setting has no effect
361 370 * Fixed: Priorities not ordered when displayed as a filter in issue list
362 371 * Fixed: can not display attached images inline in message replies
363 372 * Fixed: Boards are not deleted when project is deleted
364 373 * Fixed: trying to preview a new issue raises an exception with postgresql
365 374 * Fixed: single file 'View difference' links do not work because of duplicate slashes in url
366 375 * Fixed: inline image not displayed when including a wiki page
367 376 * Fixed: CVS duplicate key violation
368 377 * Fixed: ActiveRecord::StaleObjectError exception on closing a set of circular duplicate issues
369 378 * Fixed: custom field filters behaviour
370 379 * Fixed: Postgresql 8.3 compatibility
371 380 * Fixed: Links to repository directories don't work
372 381
373 382
374 383 == 2008-03-29 v0.7.0-rc1
375 384
376 385 * Overall activity view and feed added, link is available on the project list
377 386 * Git VCS support
378 387 * Rails 2.0 sessions cookie store compatibility
379 388 * Use project identifiers in urls instead of ids
380 389 * Default configuration data can now be loaded from the administration screen
381 390 * Administration settings screen split to tabs (email notifications options moved to 'Settings')
382 391 * Project description is now unlimited and optional
383 392 * Wiki annotate view
384 393 * Escape HTML tag in textile content
385 394 * Add Redmine links to documents, versions, attachments and repository files
386 395 * New setting to specify how many objects should be displayed on paginated lists. There are 2 ways to select a set of issues on the issue list:
387 396 * by using checkbox and/or the little pencil that will select/unselect all issues
388 397 * by clicking on the rows (but not on the links), Ctrl and Shift keys can be used to select multiple issues
389 398 * Context menu disabled on links so that the default context menu of the browser is displayed when right-clicking on a link (click anywhere else on the row to display the context menu)
390 399 * User display format is now configurable in administration settings
391 400 * Issue list now supports bulk edit/move/delete (for a set of issues that belong to the same project)
392 401 * Merged 'change status', 'edit issue' and 'add note' actions:
393 402 * Users with 'edit issues' permission can now update any property including custom fields when adding a note or changing the status
394 403 * 'Change issue status' permission removed. To change an issue status, a user just needs to have either 'Edit' or 'Add note' permissions and some workflow transitions allowed
395 404 * Details by assignees on issue summary view
396 405 * 'New issue' link in the main menu (accesskey 7). The drop-down lists to add an issue on the project overview and the issue list are removed
397 406 * Change status select box default to current status
398 407 * Preview for issue notes, news and messages
399 408 * Optional description for attachments
400 409 * 'Fixed version' label changed to 'Target version'
401 410 * Let the user choose when deleting issues with reported hours to:
402 411 * delete the hours
403 412 * assign the hours to the project
404 413 * reassign the hours to another issue
405 414 * Date range filter and pagination on time entries detail view
406 415 * Propagate time tracking to the parent project
407 416 * Switch added on the project activity view to include subprojects
408 417 * Display total estimated and spent hours on the version detail view
409 418 * Weekly time tracking block for 'My page'
410 419 * Permissions to edit time entries
411 420 * Include subprojects on the issue list, calendar, gantt and timelog by default (can be turned off is administration settings)
412 421 * Roadmap enhancements (separate related issues from wiki contents, leading h1 in version wiki pages is hidden, smaller wiki headings)
413 422 * Make versions with same date sorted by name
414 423 * Allow issue list to be sorted by target version
415 424 * Related changesets messages displayed on the issue details view
416 425 * Create a journal and send an email when an issue is closed by commit
417 426 * Add 'Author' to the available columns for the issue list
418 427 * More appropriate default sort order on sortable columns
419 428 * Add issue subject to the time entries view and issue subject, description and tracker to the csv export
420 429 * Permissions to edit issue notes
421 430 * Display date/time instead of date on files list
422 431 * Do not show Roadmap menu item if the project doesn't define any versions
423 432 * Allow longer version names (60 chars)
424 433 * Ability to copy an existing workflow when creating a new role
425 434 * Display custom fields in two columns on the issue form
426 435 * Added 'estimated time' in the csv export of the issue list
427 436 * Display the last 30 days on the activity view rather than the current month (number of days can be configured in the application settings)
428 437 * Setting for whether new projects should be public by default
429 438 * User preference to choose how comments/replies are displayed: in chronological or reverse chronological order
430 439 * Added default value for custom fields
431 440 * Added tabindex property on wiki toolbar buttons (to easily move from field to field using the tab key)
432 441 * Redirect to issue page after creating a new issue
433 442 * Wiki toolbar improvements (mainly for Firefox)
434 443 * Display wiki syntax quick ref link on all wiki textareas
435 444 * Display links to Atom feeds
436 445 * Breadcrumb nav for the forums
437 446 * Show replies when choosing to display messages in the activity
438 447 * Added 'include' macro to include another wiki page
439 448 * RedmineWikiFormatting page available as a static HTML file locally
440 449 * Wrap diff content
441 450 * Strip out email address from authors in repository screens
442 451 * Highlight the current item of the main menu
443 452 * Added simple syntax highlighters for php and java languages
444 453 * Do not show empty diffs
445 454 * Show explicit error message when the scm command failed (eg. when svn binary is not available)
446 455 * Lithuanian translation added (Sergej Jegorov)
447 456 * Ukrainan translation added (Natalia Konovka & Mykhaylo Sorochan)
448 457 * Danish translation added (Mads Vestergaard)
449 458 * Added i18n support to the jstoolbar and various settings screen
450 459 * RedCloth's glyphs no longer user
451 460 * New icons for the wiki toolbar (from http://www.famfamfam.com/lab/icons/silk/)
452 461 * The following menus can now be extended by plugins: top_menu, account_menu, application_menu
453 462 * Added a simple rake task to fetch changesets from the repositories: rake redmine:fetch_changesets
454 463 * Remove hardcoded "Redmine" strings in account related emails and use application title instead
455 464 * Mantis importer preserve bug ids
456 465 * Trac importer: Trac guide wiki pages skipped
457 466 * Trac importer: wiki attachments migration added
458 467 * Trac importer: support database schema for Trac migration
459 468 * Trac importer: support CamelCase links
460 469 * Removes the Redmine version from the footer (can be viewed on admin -> info)
461 470 * Rescue and display an error message when trying to delete a role that is in use
462 471 * Add various 'X-Redmine' headers to email notifications: X-Redmine-Host, X-Redmine-Site, X-Redmine-Project, X-Redmine-Issue-Id, -Author, -Assignee, X-Redmine-Topic-Id
463 472 * Add "--encoding utf8" option to the Mercurial "hg log" command in order to get utf8 encoded commit logs
464 473 * Fixed: Gantt and calendar not properly refreshed (fragment caching removed)
465 474 * Fixed: Textile image with style attribute cause internal server error
466 475 * Fixed: wiki TOC not rendered properly when used in an issue or document description
467 476 * Fixed: 'has already been taken' error message on username and email fields if left empty
468 477 * Fixed: non-ascii attachement filename with IE
469 478 * Fixed: wrong url for wiki syntax pop-up when Redmine urls are prefixed
470 479 * Fixed: search for all words doesn't work
471 480 * Fixed: Do not show sticky and locked checkboxes when replying to a message
472 481 * Fixed: Mantis importer: do not duplicate Mantis username in firstname and lastname if realname is blank
473 482 * Fixed: Date custom fields not displayed as specified in application settings
474 483 * Fixed: titles not escaped in the activity view
475 484 * Fixed: issue queries can not use custom fields marked as 'for all projects' in a project context
476 485 * Fixed: on calendar, gantt and in the tracker filter on the issue list, only active trackers of the project (and its sub projects) should be available
477 486 * Fixed: locked users should not receive email notifications
478 487 * Fixed: custom field selection is not saved when unchecking them all on project settings
479 488 * Fixed: can not lock a topic when creating it
480 489 * Fixed: Incorrect filtering for unset values when using 'is not' filter
481 490 * Fixed: PostgreSQL issues_seq_id not updated when using Trac importer
482 491 * Fixed: ajax pagination does not scroll up
483 492 * Fixed: error when uploading a file with no content-type specified by the browser
484 493 * Fixed: wiki and changeset links not displayed when previewing issue description or notes
485 494 * Fixed: 'LdapError: no bind result' error when authenticating
486 495 * Fixed: 'LdapError: invalid binding information' when no username/password are set on the LDAP account
487 496 * Fixed: CVS repository doesn't work if port is used in the url
488 497 * Fixed: Email notifications: host name is missing in generated links
489 498 * Fixed: Email notifications: referenced changesets, wiki pages, attachments... are not turned into links
490 499 * Fixed: Do not clear issue relations when moving an issue to another project if cross-project issue relations are allowed
491 500 * Fixed: "undefined method 'textilizable'" error on email notification when running Repository#fetch_changesets from the console
492 501 * Fixed: Do not send an email with no recipient, cc or bcc
493 502 * Fixed: fetch_changesets fails on commit comments that close 2 duplicates issues.
494 503 * Fixed: Mercurial browsing under unix-like os and for directory depth > 2
495 504 * Fixed: Wiki links with pipe can not be used in wiki tables
496 505 * Fixed: migrate_from_trac doesn't import timestamps of wiki and tickets
497 506 * Fixed: when bulk editing, setting "Assigned to" to "nobody" causes an sql error with Postgresql
498 507
499 508
500 509 == 2008-03-12 v0.6.4
501 510
502 511 * Fixed: private projects name are displayed on account/show even if the current user doesn't have access to these private projects
503 512 * Fixed: potential LDAP authentication security flaw
504 513 * Fixed: context submenus on the issue list don't show up with IE6.
505 514 * Fixed: Themes are not applied with Rails 2.0
506 515 * Fixed: crash when fetching Mercurial changesets if changeset[:files] is nil
507 516 * Fixed: Mercurial repository browsing
508 517 * Fixed: undefined local variable or method 'log' in CvsAdapter when a cvs command fails
509 518 * Fixed: not null constraints not removed with Postgresql
510 519 * Doctype set to transitional
511 520
512 521
513 522 == 2007-12-18 v0.6.3
514 523
515 524 * Fixed: upload doesn't work in 'Files' section
516 525
517 526
518 527 == 2007-12-16 v0.6.2
519 528
520 529 * Search engine: issue custom fields can now be searched
521 530 * News comments are now textilized
522 531 * Updated Japanese translation (Satoru Kurashiki)
523 532 * Updated Chinese translation (Shortie Lo)
524 533 * Fixed Rails 2.0 compatibility bugs:
525 534 * Unable to create a wiki
526 535 * Gantt and calendar error
527 536 * Trac importer error (readonly? is defined by ActiveRecord)
528 537 * Fixed: 'assigned to me' filter broken
529 538 * Fixed: crash when validation fails on issue edition with no custom fields
530 539 * Fixed: reposman "can't find group" error
531 540 * Fixed: 'LDAP account password is too long' error when leaving the field empty on creation
532 541 * Fixed: empty lines when displaying repository files with Windows style eol
533 542 * Fixed: missing body closing tag in repository annotate and entry views
534 543
535 544
536 545 == 2007-12-10 v0.6.1
537 546
538 547 * Rails 2.0 compatibility
539 548 * Custom fields can now be displayed as columns on the issue list
540 549 * Added version details view (accessible from the roadmap)
541 550 * Roadmap: more accurate completion percentage calculation (done ratio of open issues is now taken into account)
542 551 * Added per-project tracker selection. Trackers can be selected on project settings
543 552 * Anonymous users can now be allowed to create, edit, comment issues, comment news and post messages in the forums
544 553 * Forums: messages can now be edited/deleted (explicit permissions need to be given)
545 554 * Forums: topics can be locked so that no reply can be added
546 555 * Forums: topics can be marked as sticky so that they always appear at the top of the list
547 556 * Forums: attachments can now be added to replies
548 557 * Added time zone support
549 558 * Added a setting to choose the account activation strategy (available in application settings)
550 559 * Added 'Classic' theme (inspired from the v0.51 design)
551 560 * Added an alternate theme which provides issue list colorization based on issues priority
552 561 * Added Bazaar SCM adapter
553 562 * Added Annotate/Blame view in the repository browser (except for Darcs SCM)
554 563 * Diff style (inline or side by side) automatically saved as a user preference
555 564 * Added issues status changes on the activity view (by Cyril Mougel)
556 565 * Added forums topics on the activity view (disabled by default)
557 566 * Added an option on 'My account' for users who don't want to be notified of changes that they make
558 567 * Trac importer now supports mysql and postgresql databases
559 568 * Trac importer improvements (by Mat Trudel)
560 569 * 'fixed version' field can now be displayed on the issue list
561 570 * Added a couple of new formats for the 'date format' setting
562 571 * Added Traditional Chinese translation (by Shortie Lo)
563 572 * Added Russian translation (iGor kMeta)
564 573 * Project name format limitation removed (name can now contain any character)
565 574 * Project identifier maximum length changed from 12 to 20
566 575 * Changed the maximum length of LDAP account to 255 characters
567 576 * Removed the 12 characters limit on passwords
568 577 * Added wiki macros support
569 578 * Performance improvement on workflow setup screen
570 579 * More detailed html title on several views
571 580 * Custom fields can now be reordered
572 581 * Search engine: search can be restricted to an exact phrase by using quotation marks
573 582 * Added custom fields marked as 'For all projects' to the csv export of the cross project issue list
574 583 * Email notifications are now sent as Blind carbon copy by default
575 584 * Fixed: all members (including non active) should be deleted when deleting a project
576 585 * Fixed: Error on wiki syntax link (accessible from wiki/edit)
577 586 * Fixed: 'quick jump to a revision' form on the revisions list
578 587 * Fixed: error on admin/info if there's more than 1 plugin installed
579 588 * Fixed: svn or ldap password can be found in clear text in the html source in editing mode
580 589 * Fixed: 'Assigned to' drop down list is not sorted
581 590 * Fixed: 'View all issues' link doesn't work on issues/show
582 591 * Fixed: error on account/register when validation fails
583 592 * Fixed: Error when displaying the issue list if a float custom field is marked as 'used as filter'
584 593 * Fixed: Mercurial adapter breaks on missing :files entry in changeset hash (James Britt)
585 594 * Fixed: Wrong feed URLs on the home page
586 595 * Fixed: Update of time entry fails when the issue has been moved to an other project
587 596 * Fixed: Error when moving an issue without changing its tracker (Postgresql)
588 597 * Fixed: Changes not recorded when using :pserver string (CVS adapter)
589 598 * Fixed: admin should be able to move issues to any project
590 599 * Fixed: adding an attachment is not possible when changing the status of an issue
591 600 * Fixed: No mime-types in documents/files downloading
592 601 * Fixed: error when sorting the messages if there's only one board for the project
593 602 * Fixed: 'me' doesn't appear in the drop down filters on a project issue list.
594 603
595 604 == 2007-11-04 v0.6.0
596 605
597 606 * Permission model refactoring.
598 607 * Permissions: there are now 2 builtin roles that can be used to specify permissions given to other users than members of projects
599 608 * Permissions: some permissions (eg. browse the repository) can be removed for certain roles
600 609 * Permissions: modules (eg. issue tracking, news, documents...) can be enabled/disabled at project level
601 610 * Added Mantis and Trac importers
602 611 * New application layout
603 612 * Added "Bulk edit" functionality on the issue list
604 613 * More flexible mail notifications settings at user level
605 614 * Added AJAX based context menu on the project issue list that provide shortcuts for editing, re-assigning, changing the status or the priority, moving or deleting an issue
606 615 * Added the hability to copy an issue. It can be done from the "issue/show" view or from the context menu on the issue list
607 616 * Added the ability to customize issue list columns (at application level or for each saved query)
608 617 * Overdue versions (date reached and open issues > 0) are now always displayed on the roadmap
609 618 * Added the ability to rename wiki pages (specific permission required)
610 619 * Search engines now supports pagination. Results are sorted in reverse chronological order
611 620 * Added "Estimated hours" attribute on issues
612 621 * A category with assigned issue can now be deleted. 2 options are proposed: remove assignments or reassign issues to another category
613 622 * Forum notifications are now also sent to the authors of the thread, even if they donοΏ½t watch the board
614 623 * Added an application setting to specify the application protocol (http or https) used to generate urls in emails
615 624 * Gantt chart: now starts at the current month by default
616 625 * Gantt chart: month count and zoom factor are automatically saved as user preferences
617 626 * Wiki links can now refer to other project wikis
618 627 * Added wiki index by date
619 628 * Added preview on add/edit issue form
620 629 * Emails footer can now be customized from the admin interface (Admin -> Email notifications)
621 630 * Default encodings for repository files can now be set in application settings (used to convert files content and diff to UTF-8 so that theyοΏ½re properly displayed)
622 631 * Calendar: first day of week can now be set in lang files
623 632 * Automatic closing of duplicate issues
624 633 * Added a cross-project issue list
625 634 * AJAXified the SCM browser (tree view)
626 635 * Pretty URL for the repository browser (Cyril Mougel)
627 636 * Search engine: added a checkbox to search titles only
628 637 * Added "% done" in the filter list
629 638 * Enumerations: values can now be reordered and a default value can be specified (eg. default issue priority)
630 639 * Added some accesskeys
631 640 * Added "Float" as a custom field format
632 641 * Added basic Theme support
633 642 * Added the ability to set the οΏ½done ratioοΏ½ of issues fixed by commit (Nikolay Solakov)
634 643 * Added custom fields in issue related mail notifications
635 644 * Email notifications are now sent in plain text and html
636 645 * Gantt chart can now be exported to a graphic file (png). This functionality is only available if RMagick is installed.
637 646 * Added syntax highlightment for repository files and wiki
638 647 * Improved automatic Redmine links
639 648 * Added automatic table of content support on wiki pages
640 649 * Added radio buttons on the documents list to sort documents by category, date, title or author
641 650 * Added basic plugin support, with a sample plugin
642 651 * Added a link to add a new category when creating or editing an issue
643 652 * Added a "Assignable" boolean on the Role model. If unchecked, issues can not be assigned to users having this role.
644 653 * Added an option to be able to relate issues in different projects
645 654 * Added the ability to move issues (to another project) without changing their trackers.
646 655 * Atom feeds added on project activity, news and changesets
647 656 * Added the ability to reset its own RSS access key
648 657 * Main project list now displays root projects with their subprojects
649 658 * Added anchor links to issue notes
650 659 * Added reposman Ruby version. This script can now register created repositories in Redmine (Nicolas Chuche)
651 660 * Issue notes are now included in search
652 661 * Added email sending test functionality
653 662 * Added LDAPS support for LDAP authentication
654 663 * Removed hard-coded URLs in mail templates
655 664 * Subprojects are now grouped by projects in the navigation drop-down menu
656 665 * Added a new value for date filters: this week
657 666 * Added cache for application settings
658 667 * Added Polish translation (Tomasz Gawryl)
659 668 * Added Czech translation (Jan Kadlecek)
660 669 * Added Romanian translation (Csongor Bartus)
661 670 * Added Hebrew translation (Bob Builder)
662 671 * Added Serbian translation (Dragan Matic)
663 672 * Added Korean translation (Choi Jong Yoon)
664 673 * Fixed: the link to delete issue relations is displayed even if the user is not authorized to delete relations
665 674 * Performance improvement on calendar and gantt
666 675 * Fixed: wiki preview doesnοΏ½t work on long entries
667 676 * Fixed: queries with multiple custom fields return no result
668 677 * Fixed: Can not authenticate user against LDAP if its DN contains non-ascii characters
669 678 * Fixed: URL with ~ broken in wiki formatting
670 679 * Fixed: some quotation marks are rendered as strange characters in pdf
671 680
672 681
673 682 == 2007-07-15 v0.5.1
674 683
675 684 * per project forums added
676 685 * added the ability to archive projects
677 686 * added οΏ½WatchοΏ½ functionality on issues. It allows users to receive notifications about issue changes
678 687 * custom fields for issues can now be used as filters on issue list
679 688 * added per user custom queries
680 689 * commit messages are now scanned for referenced or fixed issue IDs (keywords defined in Admin -> Settings)
681 690 * projects list now shows the list of public projects and private projects for which the user is a member
682 691 * versions can now be created with no date
683 692 * added issue count details for versions on Reports view
684 693 * added time report, by member/activity/tracker/version and year/month/week for the selected period
685 694 * each category can now be associated to a user, so that new issues in that category are automatically assigned to that user
686 695 * added autologin feature (disabled by default)
687 696 * optimistic locking added for wiki edits
688 697 * added wiki diff
689 698 * added the ability to destroy wiki pages (requires permission)
690 699 * a wiki page can now be attached to each version, and displayed on the roadmap
691 700 * attachments can now be added to wiki pages (original patch by Pavol Murin) and displayed online
692 701 * added an option to see all versions in the roadmap view (including completed ones)
693 702 * added basic issue relations
694 703 * added the ability to log time when changing an issue status
695 704 * account information can now be sent to the user when creating an account
696 705 * author and assignee of an issue always receive notifications (even if they turned of mail notifications)
697 706 * added a quick search form in page header
698 707 * added 'me' value for 'assigned to' and 'author' query filters
699 708 * added a link on revision screen to see the entire diff for the revision
700 709 * added last commit message for each entry in repository browser
701 710 * added the ability to view a file diff with free to/from revision selection.
702 711 * text files can now be viewed online when browsing the repository
703 712 * added basic support for other SCM: CVS (Ralph Vater), Mercurial and Darcs
704 713 * added fragment caching for svn diffs
705 714 * added fragment caching for calendar and gantt views
706 715 * login field automatically focused on login form
707 716 * subproject name displayed on issue list, calendar and gantt
708 717 * added an option to choose the date format: language based or ISO 8601
709 718 * added a simple mail handler. It lets users add notes to an existing issue by replying to the initial notification email.
710 719 * a 403 error page is now displayed (instead of a blank page) when trying to access a protected page
711 720 * added portuguese translation (Joao Carlos Clementoni)
712 721 * added partial online help japanese translation (Ken Date)
713 722 * added bulgarian translation (Nikolay Solakov)
714 723 * added dutch translation (Linda van den Brink)
715 724 * added swedish translation (Thomas Habets)
716 725 * italian translation update (Alessio Spadaro)
717 726 * japanese translation update (Satoru Kurashiki)
718 727 * fixed: error on history atom feed when thereοΏ½s no notes on an issue change
719 728 * fixed: error in journalizing an issue with longtext custom fields (Postgresql)
720 729 * fixed: creation of Oracle schema
721 730 * fixed: last day of the month not included in project activity
722 731 * fixed: files with an apostrophe in their names can't be accessed in SVN repository
723 732 * fixed: performance issue on RepositoriesController#revisions when a changeset has a great number of changes (eg. 100,000)
724 733 * fixed: open/closed issue counts are always 0 on reports view (postgresql)
725 734 * fixed: date query filters (wrong results and sql error with postgresql)
726 735 * fixed: confidentiality issue on account/show (private project names displayed to anyone)
727 736 * fixed: Long text custom fields displayed without line breaks
728 737 * fixed: Error when editing the wokflow after deleting a status
729 738 * fixed: SVN commit dates are now stored as local time
730 739
731 740
732 741 == 2007-04-11 v0.5.0
733 742
734 743 * added per project Wiki
735 744 * added rss/atom feeds at project level (custom queries can be used as feeds)
736 745 * added search engine (search in issues, news, commits, wiki pages, documents)
737 746 * simple time tracking functionality added
738 747 * added version due dates on calendar and gantt
739 748 * added subprojects issue count on project Reports page
740 749 * added the ability to copy an existing workflow when creating a new tracker
741 750 * added the ability to include subprojects on calendar and gantt
742 751 * added the ability to select trackers to display on calendar and gantt (Jeffrey Jones)
743 752 * added side by side svn diff view (Cyril Mougel)
744 753 * added back subproject filter on issue list
745 754 * added permissions report in admin area
746 755 * added a status filter on users list
747 756 * support for password-protected SVN repositories
748 757 * SVN commits are now stored in the database
749 758 * added simple svn statistics SVG graphs
750 759 * progress bars for roadmap versions (Nick Read)
751 760 * issue history now shows file uploads and deletions
752 761 * #id patterns are turned into links to issues in descriptions and commit messages
753 762 * japanese translation added (Satoru Kurashiki)
754 763 * chinese simplified translation added (Andy Wu)
755 764 * italian translation added (Alessio Spadaro)
756 765 * added scripts to manage SVN repositories creation and user access control using ssh+svn (Nicolas Chuche)
757 766 * better calendar rendering time
758 767 * fixed migration scripts to work with mysql 5 running in strict mode
759 768 * fixed: error when clicking "add" with no block selected on my/page_layout
760 769 * fixed: hard coded links in navigation bar
761 770 * fixed: table_name pre/suffix support
762 771
763 772
764 773 == 2007-02-18 v0.4.2
765 774
766 775 * Rails 1.2 is now required
767 776 * settings are now stored in the database and editable through the application in: Admin -> Settings (config_custom.rb is no longer used)
768 777 * added project roadmap view
769 778 * mail notifications added when a document, a file or an attachment is added
770 779 * tooltips added on Gantt chart and calender to view the details of the issues
771 780 * ability to set the sort order for roles, trackers, issue statuses
772 781 * added missing fields to csv export: priority, start date, due date, done ratio
773 782 * added total number of issues per tracker on project overview
774 783 * all icons replaced (new icons are based on GPL icon set: "KDE Crystal Diamond 2.5" -by paolino- and "kNeu! Alpha v0.1" -by Pablo Fabregat-)
775 784 * added back "fixed version" field on issue screen and in filters
776 785 * project settings screen split in 4 tabs
777 786 * custom fields screen split in 3 tabs (one for each kind of custom field)
778 787 * multiple issues pdf export now rendered as a table
779 788 * added a button on users/list to manually activate an account
780 789 * added a setting option to disable "password lost" functionality
781 790 * added a setting option to set max number of issues in csv/pdf exports
782 791 * fixed: subprojects count is always 0 on projects list
783 792 * fixed: locked users are proposed when adding a member to a project
784 793 * fixed: setting an issue status as default status leads to an sql error with SQLite
785 794 * fixed: unable to delete an issue status even if it's not used yet
786 795 * fixed: filters ignored when exporting a predefined query to csv/pdf
787 796 * fixed: crash when french "issue_edit" email notification is sent
788 797 * fixed: hide mail preference not saved (my/account)
789 798 * fixed: crash when a new user try to edit its "my page" layout
790 799
791 800
792 801 == 2007-01-03 v0.4.1
793 802
794 803 * fixed: emails have no recipient when one of the project members has notifications disabled
795 804
796 805
797 806 == 2007-01-02 v0.4.0
798 807
799 808 * simple SVN browser added (just needs svn binaries in PATH)
800 809 * comments can now be added on news
801 810 * "my page" is now customizable
802 811 * more powerfull and savable filters for issues lists
803 812 * improved issues change history
804 813 * new functionality: move an issue to another project or tracker
805 814 * new functionality: add a note to an issue
806 815 * new report: project activity
807 816 * "start date" and "% done" fields added on issues
808 817 * project calendar added
809 818 * gantt chart added (exportable to pdf)
810 819 * single/multiple issues pdf export added
811 820 * issues reports improvements
812 821 * multiple file upload for issues, documents and files
813 822 * option to set maximum size of uploaded files
814 823 * textile formating of issue and news descritions (RedCloth required)
815 824 * integration of DotClear jstoolbar for textile formatting
816 825 * calendar date picker for date fields (LGPL DHTML Calendar http://sourceforge.net/projects/jscalendar)
817 826 * new filter in issues list: Author
818 827 * ajaxified paginators
819 828 * news rss feed added
820 829 * option to set number of results per page on issues list
821 830 * localized csv separator (comma/semicolon)
822 831 * csv output encoded to ISO-8859-1
823 832 * user custom field displayed on account/show
824 833 * default configuration improved (default roles, trackers, status, permissions and workflows)
825 834 * language for default configuration data can now be chosen when running 'load_default_data' task
826 835 * javascript added on custom field form to show/hide fields according to the format of custom field
827 836 * fixed: custom fields not in csv exports
828 837 * fixed: project settings now displayed according to user's permissions
829 838 * fixed: application error when no version is selected on projects/add_file
830 839 * fixed: public actions not authorized for members of non public projects
831 840 * fixed: non public projects were shown on welcome screen even if current user is not a member
832 841
833 842
834 843 == 2006-10-08 v0.3.0
835 844
836 845 * user authentication against multiple LDAP (optional)
837 846 * token based "lost password" functionality
838 847 * user self-registration functionality (optional)
839 848 * custom fields now available for issues, users and projects
840 849 * new custom field format "text" (displayed as a textarea field)
841 850 * project & administration drop down menus in navigation bar for quicker access
842 851 * text formatting is preserved for long text fields (issues, projects and news descriptions)
843 852 * urls and emails are turned into clickable links in long text fields
844 853 * "due date" field added on issues
845 854 * tracker selection filter added on change log
846 855 * Localization plugin replaced with GLoc 1.1.0 (iconv required)
847 856 * error messages internationalization
848 857 * german translation added (thanks to Karim Trott)
849 858 * data locking for issues to prevent update conflicts (using ActiveRecord builtin optimistic locking)
850 859 * new filter in issues list: "Fixed version"
851 860 * active filters are displayed with colored background on issues list
852 861 * custom configuration is now defined in config/config_custom.rb
853 862 * user object no more stored in session (only user_id)
854 863 * news summary field is no longer required
855 864 * tables and forms redesign
856 865 * Fixed: boolean custom field not working
857 866 * Fixed: error messages for custom fields are not displayed
858 867 * Fixed: invalid custom fields should have a red border
859 868 * Fixed: custom fields values are not validated on issue update
860 869 * Fixed: unable to choose an empty value for 'List' custom fields
861 870 * Fixed: no issue categories sorting
862 871 * Fixed: incorrect versions sorting
863 872
864 873
865 874 == 2006-07-12 - v0.2.2
866 875
867 876 * Fixed: bug in "issues list"
868 877
869 878
870 879 == 2006-07-09 - v0.2.1
871 880
872 881 * new databases supported: Oracle, PostgreSQL, SQL Server
873 882 * projects/subprojects hierarchy (1 level of subprojects only)
874 883 * environment information display in admin/info
875 884 * more filter options in issues list (rev6)
876 885 * default language based on browser settings (Accept-Language HTTP header)
877 886 * issues list exportable to CSV (rev6)
878 887 * simple_format and auto_link on long text fields
879 888 * more data validations
880 889 * Fixed: error when all mail notifications are unchecked in admin/mail_options
881 890 * Fixed: all project news are displayed on project summary
882 891 * Fixed: Can't change user password in users/edit
883 892 * Fixed: Error on tables creation with PostgreSQL (rev5)
884 893 * Fixed: SQL error in "issue reports" view with PostgreSQL (rev5)
885 894
886 895
887 896 == 2006-06-25 - v0.1.0
888 897
889 898 * multiple users/multiple projects
890 899 * role based access control
891 900 * issue tracking system
892 901 * fully customizable workflow
893 902 * documents/files repository
894 903 * email notifications on issue creation and update
895 904 * multilanguage support (except for error messages):english, french, spanish
896 905 * online manual in french (unfinished)
@@ -1,1177 +1,1177
1 1 # vim:ts=4:sw=4:
2 2 # = RedCloth - Textile and Markdown Hybrid for Ruby
3 3 #
4 4 # Homepage:: http://whytheluckystiff.net/ruby/redcloth/
5 5 # Author:: why the lucky stiff (http://whytheluckystiff.net/)
6 6 # Copyright:: (cc) 2004 why the lucky stiff (and his puppet organizations.)
7 7 # License:: BSD
8 8 #
9 9 # (see http://hobix.com/textile/ for a Textile Reference.)
10 10 #
11 11 # Based on (and also inspired by) both:
12 12 #
13 13 # PyTextile: http://diveintomark.org/projects/textile/textile.py.txt
14 14 # Textism for PHP: http://www.textism.com/tools/textile/
15 15 #
16 16 #
17 17
18 18 # = RedCloth
19 19 #
20 20 # RedCloth is a Ruby library for converting Textile and/or Markdown
21 21 # into HTML. You can use either format, intermingled or separately.
22 22 # You can also extend RedCloth to honor your own custom text stylings.
23 23 #
24 24 # RedCloth users are encouraged to use Textile if they are generating
25 25 # HTML and to use Markdown if others will be viewing the plain text.
26 26 #
27 27 # == What is Textile?
28 28 #
29 29 # Textile is a simple formatting style for text
30 30 # documents, loosely based on some HTML conventions.
31 31 #
32 32 # == Sample Textile Text
33 33 #
34 34 # h2. This is a title
35 35 #
36 36 # h3. This is a subhead
37 37 #
38 38 # This is a bit of paragraph.
39 39 #
40 40 # bq. This is a blockquote.
41 41 #
42 42 # = Writing Textile
43 43 #
44 44 # A Textile document consists of paragraphs. Paragraphs
45 45 # can be specially formatted by adding a small instruction
46 46 # to the beginning of the paragraph.
47 47 #
48 48 # h[n]. Header of size [n].
49 49 # bq. Blockquote.
50 50 # # Numeric list.
51 51 # * Bulleted list.
52 52 #
53 53 # == Quick Phrase Modifiers
54 54 #
55 55 # Quick phrase modifiers are also included, to allow formatting
56 56 # of small portions of text within a paragraph.
57 57 #
58 58 # \_emphasis\_
59 59 # \_\_italicized\_\_
60 60 # \*strong\*
61 61 # \*\*bold\*\*
62 62 # ??citation??
63 63 # -deleted text-
64 64 # +inserted text+
65 65 # ^superscript^
66 66 # ~subscript~
67 67 # @code@
68 68 # %(classname)span%
69 69 #
70 70 # ==notextile== (leave text alone)
71 71 #
72 72 # == Links
73 73 #
74 74 # To make a hypertext link, put the link text in "quotation
75 75 # marks" followed immediately by a colon and the URL of the link.
76 76 #
77 77 # Optional: text in (parentheses) following the link text,
78 78 # but before the closing quotation mark, will become a Title
79 79 # attribute for the link, visible as a tool tip when a cursor is above it.
80 80 #
81 81 # Example:
82 82 #
83 83 # "This is a link (This is a title) ":http://www.textism.com
84 84 #
85 85 # Will become:
86 86 #
87 87 # <a href="http://www.textism.com" title="This is a title">This is a link</a>
88 88 #
89 89 # == Images
90 90 #
91 91 # To insert an image, put the URL for the image inside exclamation marks.
92 92 #
93 93 # Optional: text that immediately follows the URL in (parentheses) will
94 94 # be used as the Alt text for the image. Images on the web should always
95 95 # have descriptive Alt text for the benefit of readers using non-graphical
96 96 # browsers.
97 97 #
98 98 # Optional: place a colon followed by a URL immediately after the
99 99 # closing ! to make the image into a link.
100 100 #
101 101 # Example:
102 102 #
103 103 # !http://www.textism.com/common/textist.gif(Textist)!
104 104 #
105 105 # Will become:
106 106 #
107 107 # <img src="http://www.textism.com/common/textist.gif" alt="Textist" />
108 108 #
109 109 # With a link:
110 110 #
111 111 # !/common/textist.gif(Textist)!:http://textism.com
112 112 #
113 113 # Will become:
114 114 #
115 115 # <a href="http://textism.com"><img src="/common/textist.gif" alt="Textist" /></a>
116 116 #
117 117 # == Defining Acronyms
118 118 #
119 119 # HTML allows authors to define acronyms via the tag. The definition appears as a
120 120 # tool tip when a cursor hovers over the acronym. A crucial aid to clear writing,
121 121 # this should be used at least once for each acronym in documents where they appear.
122 122 #
123 123 # To quickly define an acronym in Textile, place the full text in (parentheses)
124 124 # immediately following the acronym.
125 125 #
126 126 # Example:
127 127 #
128 128 # ACLU(American Civil Liberties Union)
129 129 #
130 130 # Will become:
131 131 #
132 132 # <acronym title="American Civil Liberties Union">ACLU</acronym>
133 133 #
134 134 # == Adding Tables
135 135 #
136 136 # In Textile, simple tables can be added by seperating each column by
137 137 # a pipe.
138 138 #
139 139 # |a|simple|table|row|
140 140 # |And|Another|table|row|
141 141 #
142 142 # Attributes are defined by style definitions in parentheses.
143 143 #
144 144 # table(border:1px solid black).
145 145 # (background:#ddd;color:red). |{}| | | |
146 146 #
147 147 # == Using RedCloth
148 148 #
149 149 # RedCloth is simply an extension of the String class, which can handle
150 150 # Textile formatting. Use it like a String and output HTML with its
151 151 # RedCloth#to_html method.
152 152 #
153 153 # doc = RedCloth.new "
154 154 #
155 155 # h2. Test document
156 156 #
157 157 # Just a simple test."
158 158 #
159 159 # puts doc.to_html
160 160 #
161 161 # By default, RedCloth uses both Textile and Markdown formatting, with
162 162 # Textile formatting taking precedence. If you want to turn off Markdown
163 163 # formatting, to boost speed and limit the processor:
164 164 #
165 165 # class RedCloth::Textile.new( str )
166 166
167 167 class RedCloth3 < String
168 168
169 169 VERSION = '3.0.4'
170 170 DEFAULT_RULES = [:textile, :markdown]
171 171
172 172 #
173 173 # Two accessor for setting security restrictions.
174 174 #
175 175 # This is a nice thing if you're using RedCloth for
176 176 # formatting in public places (e.g. Wikis) where you
177 177 # don't want users to abuse HTML for bad things.
178 178 #
179 179 # If +:filter_html+ is set, HTML which wasn't
180 180 # created by the Textile processor will be escaped.
181 181 #
182 182 # If +:filter_styles+ is set, it will also disable
183 183 # the style markup specifier. ('{color: red}')
184 184 #
185 185 attr_accessor :filter_html, :filter_styles
186 186
187 187 #
188 188 # Accessor for toggling hard breaks.
189 189 #
190 190 # If +:hard_breaks+ is set, single newlines will
191 191 # be converted to HTML break tags. This is the
192 192 # default behavior for traditional RedCloth.
193 193 #
194 194 attr_accessor :hard_breaks
195 195
196 196 # Accessor for toggling lite mode.
197 197 #
198 198 # In lite mode, block-level rules are ignored. This means
199 199 # that tables, paragraphs, lists, and such aren't available.
200 200 # Only the inline markup for bold, italics, entities and so on.
201 201 #
202 202 # r = RedCloth.new( "And then? She *fell*!", [:lite_mode] )
203 203 # r.to_html
204 204 # #=> "And then? She <strong>fell</strong>!"
205 205 #
206 206 attr_accessor :lite_mode
207 207
208 208 #
209 209 # Accessor for toggling span caps.
210 210 #
211 211 # Textile places `span' tags around capitalized
212 212 # words by default, but this wreaks havoc on Wikis.
213 213 # If +:no_span_caps+ is set, this will be
214 214 # suppressed.
215 215 #
216 216 attr_accessor :no_span_caps
217 217
218 218 #
219 219 # Establishes the markup predence. Available rules include:
220 220 #
221 221 # == Textile Rules
222 222 #
223 223 # The following textile rules can be set individually. Or add the complete
224 224 # set of rules with the single :textile rule, which supplies the rule set in
225 225 # the following precedence:
226 226 #
227 227 # refs_textile:: Textile references (i.e. [hobix]http://hobix.com/)
228 228 # block_textile_table:: Textile table block structures
229 229 # block_textile_lists:: Textile list structures
230 230 # block_textile_prefix:: Textile blocks with prefixes (i.e. bq., h2., etc.)
231 231 # inline_textile_image:: Textile inline images
232 232 # inline_textile_link:: Textile inline links
233 233 # inline_textile_span:: Textile inline spans
234 234 # glyphs_textile:: Textile entities (such as em-dashes and smart quotes)
235 235 #
236 236 # == Markdown
237 237 #
238 238 # refs_markdown:: Markdown references (for example: [hobix]: http://hobix.com/)
239 239 # block_markdown_setext:: Markdown setext headers
240 240 # block_markdown_atx:: Markdown atx headers
241 241 # block_markdown_rule:: Markdown horizontal rules
242 242 # block_markdown_bq:: Markdown blockquotes
243 243 # block_markdown_lists:: Markdown lists
244 244 # inline_markdown_link:: Markdown links
245 245 attr_accessor :rules
246 246
247 247 # Returns a new RedCloth object, based on _string_ and
248 248 # enforcing all the included _restrictions_.
249 249 #
250 250 # r = RedCloth.new( "h1. A <b>bold</b> man", [:filter_html] )
251 251 # r.to_html
252 252 # #=>"<h1>A &lt;b&gt;bold&lt;/b&gt; man</h1>"
253 253 #
254 254 def initialize( string, restrictions = [] )
255 255 restrictions.each { |r| method( "#{ r }=" ).call( true ) }
256 256 super( string )
257 257 end
258 258
259 259 #
260 260 # Generates HTML from the Textile contents.
261 261 #
262 262 # r = RedCloth.new( "And then? She *fell*!" )
263 263 # r.to_html( true )
264 264 # #=>"And then? She <strong>fell</strong>!"
265 265 #
266 266 def to_html( *rules )
267 267 rules = DEFAULT_RULES if rules.empty?
268 268 # make our working copy
269 269 text = self.dup
270 270
271 271 @urlrefs = {}
272 272 @shelf = []
273 273 textile_rules = [:refs_textile, :block_textile_table, :block_textile_lists,
274 274 :block_textile_prefix, :inline_textile_image, :inline_textile_link,
275 275 :inline_textile_code, :inline_textile_span, :glyphs_textile]
276 276 markdown_rules = [:refs_markdown, :block_markdown_setext, :block_markdown_atx, :block_markdown_rule,
277 277 :block_markdown_bq, :block_markdown_lists,
278 278 :inline_markdown_reflink, :inline_markdown_link]
279 279 @rules = rules.collect do |rule|
280 280 case rule
281 281 when :markdown
282 282 markdown_rules
283 283 when :textile
284 284 textile_rules
285 285 else
286 286 rule
287 287 end
288 288 end.flatten
289 289
290 290 # standard clean up
291 291 incoming_entities text
292 292 clean_white_space text
293 293
294 294 # start processor
295 295 @pre_list = []
296 296 rip_offtags text
297 297 no_textile text
298 298 escape_html_tags text
299 299 hard_break text
300 300 unless @lite_mode
301 301 refs text
302 302 # need to do this before text is split by #blocks
303 303 block_textile_quotes text
304 304 blocks text
305 305 end
306 306 inline text
307 307 smooth_offtags text
308 308
309 309 retrieve text
310 310
311 311 text.gsub!( /<\/?notextile>/, '' )
312 312 text.gsub!( /x%x%/, '&#38;' )
313 313 clean_html text if filter_html
314 314 text.strip!
315 315 text
316 316
317 317 end
318 318
319 319 #######
320 320 private
321 321 #######
322 322 #
323 323 # Mapping of 8-bit ASCII codes to HTML numerical entity equivalents.
324 324 # (from PyTextile)
325 325 #
326 326 TEXTILE_TAGS =
327 327
328 328 [[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230],
329 329 [134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249],
330 330 [140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217],
331 331 [147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732],
332 332 [153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]].
333 333
334 334 collect! do |a, b|
335 335 [a.chr, ( b.zero? and "" or "&#{ b };" )]
336 336 end
337 337
338 338 #
339 339 # Regular expressions to convert to HTML.
340 340 #
341 341 A_HLGN = /(?:(?:<>|<|>|\=|[()]+)+)/
342 342 A_VLGN = /[\-^~]/
343 343 C_CLAS = '(?:\([^)]+\))'
344 344 C_LNGE = '(?:\[[^\[\]]+\])'
345 345 C_STYL = '(?:\{[^}]+\})'
346 346 S_CSPN = '(?:\\\\\d+)'
347 347 S_RSPN = '(?:/\d+)'
348 348 A = "(?:#{A_HLGN}?#{A_VLGN}?|#{A_VLGN}?#{A_HLGN}?)"
349 349 S = "(?:#{S_CSPN}?#{S_RSPN}|#{S_RSPN}?#{S_CSPN}?)"
350 350 C = "(?:#{C_CLAS}?#{C_STYL}?#{C_LNGE}?|#{C_STYL}?#{C_LNGE}?#{C_CLAS}?|#{C_LNGE}?#{C_STYL}?#{C_CLAS}?)"
351 351 # PUNCT = Regexp::quote( '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' )
352 352 PUNCT = Regexp::quote( '!"#$%&\'*+,-./:;=?@\\^_`|~' )
353 353 PUNCT_NOQ = Regexp::quote( '!"#$&\',./:;=?@\\`|' )
354 354 PUNCT_Q = Regexp::quote( '*-_+^~%' )
355 355 HYPERLINK = '(\S+?)([^\w\s/;=\?]*?)(?=\s|<|$)'
356 356
357 357 # Text markup tags, don't conflict with block tags
358 358 SIMPLE_HTML_TAGS = [
359 359 'tt', 'b', 'i', 'big', 'small', 'em', 'strong', 'dfn', 'code',
360 360 'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'a', 'img', 'br',
361 361 'br', 'map', 'q', 'sub', 'sup', 'span', 'bdo'
362 362 ]
363 363
364 364 QTAGS = [
365 365 ['**', 'b', :limit],
366 366 ['*', 'strong', :limit],
367 367 ['??', 'cite', :limit],
368 368 ['-', 'del', :limit],
369 369 ['__', 'i', :limit],
370 370 ['_', 'em', :limit],
371 371 ['%', 'span', :limit],
372 372 ['+', 'ins', :limit],
373 373 ['^', 'sup', :limit],
374 374 ['~', 'sub', :limit]
375 375 ]
376 376 QTAGS.collect! do |rc, ht, rtype|
377 377 rcq = Regexp::quote rc
378 378 re =
379 379 case rtype
380 380 when :limit
381 381 /(^|[>\s\(])
382 382 (#{rcq})
383 383 (#{C})
384 384 (?::(\S+?))?
385 385 (\w|[^\s\-].*?[^\s\-])
386 386 #{rcq}
387 387 (?=[[:punct:]]|\s|\)|$)/x
388 388 else
389 389 /(#{rcq})
390 390 (#{C})
391 391 (?::(\S+))?
392 392 (\w|[^\s\-].*?[^\s\-])
393 393 #{rcq}/xm
394 394 end
395 395 [rc, ht, re, rtype]
396 396 end
397 397
398 398 # Elements to handle
399 399 GLYPHS = [
400 400 # [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1&#8217;\2' ], # single closing
401 401 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)\'/, '\1&#8217;' ], # single closing
402 402 # [ /\'(?=[#{PUNCT_Q}]*(s\b|[\s#{PUNCT_NOQ}]))/, '&#8217;' ], # single closing
403 403 # [ /\'/, '&#8216;' ], # single opening
404 404 # [ /</, '&lt;' ], # less-than
405 405 # [ />/, '&gt;' ], # greater-than
406 406 # [ /([^\s\[{(])?"(\s|:|$)/, '\1&#8221;\2' ], # double closing
407 407 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)"/, '\1&#8221;' ], # double closing
408 408 # [ /"(?=[#{PUNCT_Q}]*[\s#{PUNCT_NOQ}])/, '&#8221;' ], # double closing
409 409 # [ /"/, '&#8220;' ], # double opening
410 410 # [ /\b( )?\.{3}/, '\1&#8230;' ], # ellipsis
411 411 # [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '<acronym title="\2">\1</acronym>' ], # 3+ uppercase acronym
412 412 # [ /(^|[^"][>\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
413 413 # [ /(\.\s)?\s?--\s?/, '\1&#8212;' ], # em dash
414 414 # [ /\s->\s/, ' &rarr; ' ], # right arrow
415 415 # [ /\s-\s/, ' &#8211; ' ], # en dash
416 416 # [ /(\d+) ?x ?(\d+)/, '\1&#215;\2' ], # dimension sign
417 417 # [ /\b ?[(\[]TM[\])]/i, '&#8482;' ], # trademark
418 418 # [ /\b ?[(\[]R[\])]/i, '&#174;' ], # registered
419 419 # [ /\b ?[(\[]C[\])]/i, '&#169;' ] # copyright
420 420 ]
421 421
422 422 H_ALGN_VALS = {
423 423 '<' => 'left',
424 424 '=' => 'center',
425 425 '>' => 'right',
426 426 '<>' => 'justify'
427 427 }
428 428
429 429 V_ALGN_VALS = {
430 430 '^' => 'top',
431 431 '-' => 'middle',
432 432 '~' => 'bottom'
433 433 }
434 434
435 435 #
436 436 # Flexible HTML escaping
437 437 #
438 438 def htmlesc( str, mode=:Quotes )
439 439 if str
440 440 str.gsub!( '&', '&amp;' )
441 441 str.gsub!( '"', '&quot;' ) if mode != :NoQuotes
442 442 str.gsub!( "'", '&#039;' ) if mode == :Quotes
443 443 str.gsub!( '<', '&lt;')
444 444 str.gsub!( '>', '&gt;')
445 445 end
446 446 str
447 447 end
448 448
449 449 # Search and replace for Textile glyphs (quotes, dashes, other symbols)
450 450 def pgl( text )
451 451 #GLYPHS.each do |re, resub, tog|
452 452 # next if tog and method( tog ).call
453 453 # text.gsub! re, resub
454 454 #end
455 455 text.gsub!(/\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/) do |m|
456 456 "<acronym title=\"#{htmlesc $2}\">#{$1}</acronym>"
457 457 end
458 458 end
459 459
460 460 # Parses Textile attribute lists and builds an HTML attribute string
461 461 def pba( text_in, element = "" )
462 462
463 463 return '' unless text_in
464 464
465 465 style = []
466 466 text = text_in.dup
467 467 if element == 'td'
468 468 colspan = $1 if text =~ /\\(\d+)/
469 469 rowspan = $1 if text =~ /\/(\d+)/
470 470 style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN
471 471 end
472 472
473 473 style << "#{ htmlesc $1 };" if text.sub!( /\{([^}]*)\}/, '' ) && !filter_styles
474 474
475 475 lang = $1 if
476 476 text.sub!( /\[([^)]+?)\]/, '' )
477 477
478 478 cls = $1 if
479 479 text.sub!( /\(([^()]+?)\)/, '' )
480 480
481 481 style << "padding-left:#{ $1.length }em;" if
482 482 text.sub!( /([(]+)/, '' )
483 483
484 484 style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' )
485 485
486 486 style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN
487 487
488 488 cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/
489 489
490 490 atts = ''
491 491 atts << " style=\"#{ style.join }\"" unless style.empty?
492 492 atts << " class=\"#{ cls }\"" unless cls.to_s.empty?
493 493 atts << " lang=\"#{ lang }\"" if lang
494 494 atts << " id=\"#{ id }\"" if id
495 495 atts << " colspan=\"#{ colspan }\"" if colspan
496 496 atts << " rowspan=\"#{ rowspan }\"" if rowspan
497 497
498 498 atts
499 499 end
500 500
501 501 TABLE_RE = /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)(\n\n|\Z)/m
502 502
503 503 # Parses a Textile table block, building HTML from the result.
504 504 def block_textile_table( text )
505 505 text.gsub!( TABLE_RE ) do |matches|
506 506
507 507 tatts, fullrow = $~[1..2]
508 508 tatts = pba( tatts, 'table' )
509 509 tatts = shelve( tatts ) if tatts
510 510 rows = []
511 511
512 512 fullrow.each_line do |row|
513 513 ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m
514 514 cells = []
515 515 row.split( /(\|)(?![^\[\|]*\]\])/ )[1..-2].each do |cell|
516 516 next if cell == '|'
517 517 ctyp = 'd'
518 518 ctyp = 'h' if cell =~ /^_/
519 519
520 520 catts = ''
521 521 catts, cell = pba( $1, 'td' ), $2 if cell =~ /^(_?#{S}#{A}#{C}\. ?)(.*)/
522 522
523 523 catts = shelve( catts ) if catts
524 524 cells << "\t\t\t<t#{ ctyp }#{ catts }>#{ cell }</t#{ ctyp }>"
525 525 end
526 526 ratts = shelve( ratts ) if ratts
527 527 rows << "\t\t<tr#{ ratts }>\n#{ cells.join( "\n" ) }\n\t\t</tr>"
528 528 end
529 529 "\t<table#{ tatts }>\n#{ rows.join( "\n" ) }\n\t</table>\n\n"
530 530 end
531 531 end
532 532
533 533 LISTS_RE = /^([#*]+?#{C} .*?)$(?![^#*])/m
534 534 LISTS_CONTENT_RE = /^([#*]+)(#{A}#{C}) (.*)$/m
535 535
536 536 # Parses Textile lists and generates HTML
537 537 def block_textile_lists( text )
538 538 text.gsub!( LISTS_RE ) do |match|
539 539 lines = match.split( /\n/ )
540 540 last_line = -1
541 541 depth = []
542 542 lines.each_with_index do |line, line_id|
543 543 if line =~ LISTS_CONTENT_RE
544 544 tl,atts,content = $~[1..3]
545 545 if depth.last
546 546 if depth.last.length > tl.length
547 547 (depth.length - 1).downto(0) do |i|
548 548 break if depth[i].length == tl.length
549 549 lines[line_id - 1] << "</li>\n\t</#{ lT( depth[i] ) }l>\n\t"
550 550 depth.pop
551 551 end
552 552 end
553 553 if depth.last and depth.last.length == tl.length
554 554 lines[line_id - 1] << '</li>'
555 555 end
556 556 end
557 557 unless depth.last == tl
558 558 depth << tl
559 559 atts = pba( atts )
560 560 atts = shelve( atts ) if atts
561 561 lines[line_id] = "\t<#{ lT(tl) }l#{ atts }>\n\t<li>#{ content }"
562 562 else
563 563 lines[line_id] = "\t\t<li>#{ content }"
564 564 end
565 565 last_line = line_id
566 566
567 567 else
568 568 last_line = line_id
569 569 end
570 570 if line_id - last_line > 1 or line_id == lines.length - 1
571 571 depth.delete_if do |v|
572 572 lines[last_line] << "</li>\n\t</#{ lT( v ) }l>"
573 573 end
574 574 end
575 575 end
576 576 lines.join( "\n" )
577 577 end
578 578 end
579 579
580 580 QUOTES_RE = /(^>+([^\n]*?)\n?)+/m
581 581 QUOTES_CONTENT_RE = /^([> ]+)(.*)$/m
582 582
583 583 def block_textile_quotes( text )
584 584 text.gsub!( QUOTES_RE ) do |match|
585 585 lines = match.split( /\n/ )
586 586 quotes = ''
587 587 indent = 0
588 588 lines.each do |line|
589 589 line =~ QUOTES_CONTENT_RE
590 590 bq,content = $1, $2
591 591 l = bq.count('>')
592 592 if l != indent
593 593 quotes << ("\n\n" + (l>indent ? '<blockquote>' * (l-indent) : '</blockquote>' * (indent-l)) + "\n\n")
594 594 indent = l
595 595 end
596 596 quotes << (content + "\n")
597 597 end
598 598 quotes << ("\n" + '</blockquote>' * indent + "\n\n")
599 599 quotes
600 600 end
601 601 end
602 602
603 603 CODE_RE = /(\W)
604 604 @
605 605 (?:\|(\w+?)\|)?
606 606 (.+?)
607 607 @
608 608 (?=\W)/x
609 609
610 610 def inline_textile_code( text )
611 611 text.gsub!( CODE_RE ) do |m|
612 612 before,lang,code,after = $~[1..4]
613 613 lang = " lang=\"#{ lang }\"" if lang
614 614 rip_offtags( "#{ before }<code#{ lang }>#{ code }</code>#{ after }" )
615 615 end
616 616 end
617 617
618 618 def lT( text )
619 619 text =~ /\#$/ ? 'o' : 'u'
620 620 end
621 621
622 622 def hard_break( text )
623 623 text.gsub!( /(.)\n(?!\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
624 624 end
625 625
626 626 BLOCKS_GROUP_RE = /\n{2,}(?! )/m
627 627
628 628 def blocks( text, deep_code = false )
629 629 text.replace( text.split( BLOCKS_GROUP_RE ).collect do |blk|
630 630 plain = blk !~ /\A[#*> ]/
631 631
632 632 # skip blocks that are complex HTML
633 633 if blk =~ /^<\/?(\w+).*>/ and not SIMPLE_HTML_TAGS.include? $1
634 634 blk
635 635 else
636 636 # search for indentation levels
637 637 blk.strip!
638 638 if blk.empty?
639 639 blk
640 640 else
641 641 code_blk = nil
642 642 blk.gsub!( /((?:\n(?:\n^ +[^\n]*)+)+)/m ) do |iblk|
643 643 flush_left iblk
644 644 blocks iblk, plain
645 645 iblk.gsub( /^(\S)/, "\t\\1" )
646 646 if plain
647 647 code_blk = iblk; ""
648 648 else
649 649 iblk
650 650 end
651 651 end
652 652
653 653 block_applied = 0
654 654 @rules.each do |rule_name|
655 655 block_applied += 1 if ( rule_name.to_s.match /^block_/ and method( rule_name ).call( blk ) )
656 656 end
657 657 if block_applied.zero?
658 658 if deep_code
659 659 blk = "\t<pre><code>#{ blk }</code></pre>"
660 660 else
661 661 blk = "\t<p>#{ blk }</p>"
662 662 end
663 663 end
664 664 # hard_break blk
665 665 blk + "\n#{ code_blk }"
666 666 end
667 667 end
668 668
669 669 end.join( "\n\n" ) )
670 670 end
671 671
672 672 def textile_bq( tag, atts, cite, content )
673 673 cite, cite_title = check_refs( cite )
674 674 cite = " cite=\"#{ cite }\"" if cite
675 675 atts = shelve( atts ) if atts
676 676 "\t<blockquote#{ cite }>\n\t\t<p#{ atts }>#{ content }</p>\n\t</blockquote>"
677 677 end
678 678
679 679 def textile_p( tag, atts, cite, content )
680 680 atts = shelve( atts ) if atts
681 681 "\t<#{ tag }#{ atts }>#{ content }</#{ tag }>"
682 682 end
683 683
684 684 alias textile_h1 textile_p
685 685 alias textile_h2 textile_p
686 686 alias textile_h3 textile_p
687 687 alias textile_h4 textile_p
688 688 alias textile_h5 textile_p
689 689 alias textile_h6 textile_p
690 690
691 691 def textile_fn_( tag, num, atts, cite, content )
692 692 atts << " id=\"fn#{ num }\" class=\"footnote\""
693 693 content = "<sup>#{ num }</sup> #{ content }"
694 694 atts = shelve( atts ) if atts
695 695 "\t<p#{ atts }>#{ content }</p>"
696 696 end
697 697
698 698 BLOCK_RE = /^(([a-z]+)(\d*))(#{A}#{C})\.(?::(\S+))? (.*)$/m
699 699
700 700 def block_textile_prefix( text )
701 701 if text =~ BLOCK_RE
702 702 tag,tagpre,num,atts,cite,content = $~[1..6]
703 703 atts = pba( atts )
704 704
705 705 # pass to prefix handler
706 706 if respond_to? "textile_#{ tag }", true
707 707 text.gsub!( $&, method( "textile_#{ tag }" ).call( tag, atts, cite, content ) )
708 708 elsif respond_to? "textile_#{ tagpre }_", true
709 709 text.gsub!( $&, method( "textile_#{ tagpre }_" ).call( tagpre, num, atts, cite, content ) )
710 710 end
711 711 end
712 712 end
713 713
714 714 SETEXT_RE = /\A(.+?)\n([=-])[=-]* *$/m
715 715 def block_markdown_setext( text )
716 716 if text =~ SETEXT_RE
717 717 tag = if $2 == "="; "h1"; else; "h2"; end
718 718 blk, cont = "<#{ tag }>#{ $1 }</#{ tag }>", $'
719 719 blocks cont
720 720 text.replace( blk + cont )
721 721 end
722 722 end
723 723
724 724 ATX_RE = /\A(\#{1,6}) # $1 = string of #'s
725 725 [ ]*
726 726 (.+?) # $2 = Header text
727 727 [ ]*
728 728 \#* # optional closing #'s (not counted)
729 729 $/x
730 730 def block_markdown_atx( text )
731 731 if text =~ ATX_RE
732 732 tag = "h#{ $1.length }"
733 733 blk, cont = "<#{ tag }>#{ $2 }</#{ tag }>\n\n", $'
734 734 blocks cont
735 735 text.replace( blk + cont )
736 736 end
737 737 end
738 738
739 739 MARKDOWN_BQ_RE = /\A(^ *> ?.+$(.+\n)*\n*)+/m
740 740
741 741 def block_markdown_bq( text )
742 742 text.gsub!( MARKDOWN_BQ_RE ) do |blk|
743 743 blk.gsub!( /^ *> ?/, '' )
744 744 flush_left blk
745 745 blocks blk
746 746 blk.gsub!( /^(\S)/, "\t\\1" )
747 747 "<blockquote>\n#{ blk }\n</blockquote>\n\n"
748 748 end
749 749 end
750 750
751 751 MARKDOWN_RULE_RE = /^(#{
752 752 ['*', '-', '_'].collect { |ch| ' ?(' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' )
753 753 })$/
754 754
755 755 def block_markdown_rule( text )
756 756 text.gsub!( MARKDOWN_RULE_RE ) do |blk|
757 757 "<hr />"
758 758 end
759 759 end
760 760
761 761 # XXX TODO XXX
762 762 def block_markdown_lists( text )
763 763 end
764 764
765 765 def inline_textile_span( text )
766 766 QTAGS.each do |qtag_rc, ht, qtag_re, rtype|
767 767 text.gsub!( qtag_re ) do |m|
768 768
769 769 case rtype
770 770 when :limit
771 771 sta,qtag,atts,cite,content = $~[1..5]
772 772 else
773 773 qtag,atts,cite,content = $~[1..4]
774 774 sta = ''
775 775 end
776 776 atts = pba( atts )
777 777 atts << " cite=\"#{ cite }\"" if cite
778 778 atts = shelve( atts ) if atts
779 779
780 780 "#{ sta }<#{ ht }#{ atts }>#{ content }</#{ ht }>"
781 781
782 782 end
783 783 end
784 784 end
785 785
786 786 LINK_RE = /
787 787 ([\s\[{(]|[#{PUNCT}])? # $pre
788 788 " # start
789 789 (#{C}) # $atts
790 790 ([^"\n]+?) # $text
791 791 \s?
792 792 (?:\(([^)]+?)\)(?="))? # $title
793 793 ":
794 794 ( # $url
795 (\/|[a-zA-Z]+:\/\/|www\.) # $proto
795 (\/|[a-zA-Z]+:\/\/|www\.|mailto:) # $proto
796 796 [\w\/]\S+?
797 797 )
798 798 (\/)? # $slash
799 799 ([^\w\=\/;\(\)]*?) # $post
800 800 (?=<|\s|$)
801 801 /x
802 802 #"
803 803 def inline_textile_link( text )
804 804 text.gsub!( LINK_RE ) do |m|
805 805 pre,atts,text,title,url,proto,slash,post = $~[1..8]
806 806
807 807 url, url_title = check_refs( url )
808 808 title ||= url_title
809 809
810 810 # Idea below : an URL with unbalanced parethesis and
811 811 # ending by ')' is put into external parenthesis
812 812 if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
813 813 url=url[0..-2] # discard closing parenth from url
814 814 post = ")"+post # add closing parenth to post
815 815 end
816 816 atts = pba( atts )
817 817 atts = " href=\"#{ url }#{ slash }\"#{ atts }"
818 818 atts << " title=\"#{ htmlesc title }\"" if title
819 819 atts = shelve( atts ) if atts
820 820
821 821 external = (url =~ /^https?:\/\//) ? ' class="external"' : ''
822 822
823 823 "#{ pre }<a#{ atts }#{ external }>#{ text }</a>#{ post }"
824 824 end
825 825 end
826 826
827 827 MARKDOWN_REFLINK_RE = /
828 828 \[([^\[\]]+)\] # $text
829 829 [ ]? # opt. space
830 830 (?:\n[ ]*)? # one optional newline followed by spaces
831 831 \[(.*?)\] # $id
832 832 /x
833 833
834 834 def inline_markdown_reflink( text )
835 835 text.gsub!( MARKDOWN_REFLINK_RE ) do |m|
836 836 text, id = $~[1..2]
837 837
838 838 if id.empty?
839 839 url, title = check_refs( text )
840 840 else
841 841 url, title = check_refs( id )
842 842 end
843 843
844 844 atts = " href=\"#{ url }\""
845 845 atts << " title=\"#{ title }\"" if title
846 846 atts = shelve( atts )
847 847
848 848 "<a#{ atts }>#{ text }</a>"
849 849 end
850 850 end
851 851
852 852 MARKDOWN_LINK_RE = /
853 853 \[([^\[\]]+)\] # $text
854 854 \( # open paren
855 855 [ \t]* # opt space
856 856 <?(.+?)>? # $href
857 857 [ \t]* # opt space
858 858 (?: # whole title
859 859 (['"]) # $quote
860 860 (.*?) # $title
861 861 \3 # matching quote
862 862 )? # title is optional
863 863 \)
864 864 /x
865 865
866 866 def inline_markdown_link( text )
867 867 text.gsub!( MARKDOWN_LINK_RE ) do |m|
868 868 text, url, quote, title = $~[1..4]
869 869
870 870 atts = " href=\"#{ url }\""
871 871 atts << " title=\"#{ title }\"" if title
872 872 atts = shelve( atts )
873 873
874 874 "<a#{ atts }>#{ text }</a>"
875 875 end
876 876 end
877 877
878 878 TEXTILE_REFS_RE = /(^ *)\[([^\[\n]+?)\](#{HYPERLINK})(?=\s|$)/
879 879 MARKDOWN_REFS_RE = /(^ *)\[([^\n]+?)\]:\s+<?(#{HYPERLINK})>?(?:\s+"((?:[^"]|\\")+)")?(?=\s|$)/m
880 880
881 881 def refs( text )
882 882 @rules.each do |rule_name|
883 883 method( rule_name ).call( text ) if rule_name.to_s.match /^refs_/
884 884 end
885 885 end
886 886
887 887 def refs_textile( text )
888 888 text.gsub!( TEXTILE_REFS_RE ) do |m|
889 889 flag, url = $~[2..3]
890 890 @urlrefs[flag.downcase] = [url, nil]
891 891 nil
892 892 end
893 893 end
894 894
895 895 def refs_markdown( text )
896 896 text.gsub!( MARKDOWN_REFS_RE ) do |m|
897 897 flag, url = $~[2..3]
898 898 title = $~[6]
899 899 @urlrefs[flag.downcase] = [url, title]
900 900 nil
901 901 end
902 902 end
903 903
904 904 def check_refs( text )
905 905 ret = @urlrefs[text.downcase] if text
906 906 ret || [text, nil]
907 907 end
908 908
909 909 IMAGE_RE = /
910 (<p>|.|^) # start of line?
910 (<p>|\s|^) # start of line?
911 911 \! # opening
912 912 (\<|\=|\>)? # optional alignment atts
913 913 (#{C}) # optional style,class atts
914 914 (?:\. )? # optional dot-space
915 915 ([^\s(!]+?) # presume this is the src
916 916 \s? # optional space
917 917 (?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title
918 918 \! # closing
919 919 (?::#{ HYPERLINK })? # optional href
920 920 /x
921 921
922 922 def inline_textile_image( text )
923 923 text.gsub!( IMAGE_RE ) do |m|
924 924 stln,algn,atts,url,title,href,href_a1,href_a2 = $~[1..8]
925 925 htmlesc title
926 926 atts = pba( atts )
927 927 atts = " src=\"#{ url }\"#{ atts }"
928 928 atts << " title=\"#{ title }\"" if title
929 929 atts << " alt=\"#{ title }\""
930 930 # size = @getimagesize($url);
931 931 # if($size) $atts.= " $size[3]";
932 932
933 933 href, alt_title = check_refs( href ) if href
934 934 url, url_title = check_refs( url )
935 935
936 936 out = ''
937 937 out << "<a#{ shelve( " href=\"#{ href }\"" ) }>" if href
938 938 out << "<img#{ shelve( atts ) } />"
939 939 out << "</a>#{ href_a1 }#{ href_a2 }" if href
940 940
941 941 if algn
942 942 algn = h_align( algn )
943 943 if stln == "<p>"
944 944 out = "<p style=\"float:#{ algn }\">#{ out }"
945 945 else
946 946 out = "#{ stln }<div style=\"float:#{ algn }\">#{ out }</div>"
947 947 end
948 948 else
949 949 out = stln + out
950 950 end
951 951
952 952 out
953 953 end
954 954 end
955 955
956 956 def shelve( val )
957 957 @shelf << val
958 958 " :redsh##{ @shelf.length }:"
959 959 end
960 960
961 961 def retrieve( text )
962 962 @shelf.each_with_index do |r, i|
963 963 text.gsub!( " :redsh##{ i + 1 }:", r )
964 964 end
965 965 end
966 966
967 967 def incoming_entities( text )
968 968 ## turn any incoming ampersands into a dummy character for now.
969 969 ## This uses a negative lookahead for alphanumerics followed by a semicolon,
970 970 ## implying an incoming html entity, to be skipped
971 971
972 972 text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" )
973 973 end
974 974
975 975 def no_textile( text )
976 976 text.gsub!( /(^|\s)==([^=]+.*?)==(\s|$)?/,
977 977 '\1<notextile>\2</notextile>\3' )
978 978 text.gsub!( /^ *==([^=]+.*?)==/m,
979 979 '\1<notextile>\2</notextile>\3' )
980 980 end
981 981
982 982 def clean_white_space( text )
983 983 # normalize line breaks
984 984 text.gsub!( /\r\n/, "\n" )
985 985 text.gsub!( /\r/, "\n" )
986 986 text.gsub!( /\t/, ' ' )
987 987 text.gsub!( /^ +$/, '' )
988 988 text.gsub!( /\n{3,}/, "\n\n" )
989 989 text.gsub!( /"$/, "\" " )
990 990
991 991 # if entire document is indented, flush
992 992 # to the left side
993 993 flush_left text
994 994 end
995 995
996 996 def flush_left( text )
997 997 indt = 0
998 998 if text =~ /^ /
999 999 while text !~ /^ {#{indt}}\S/
1000 1000 indt += 1
1001 1001 end unless text.empty?
1002 1002 if indt.nonzero?
1003 1003 text.gsub!( /^ {#{indt}}/, '' )
1004 1004 end
1005 1005 end
1006 1006 end
1007 1007
1008 1008 def footnote_ref( text )
1009 1009 text.gsub!( /\b\[([0-9]+?)\](\s)?/,
1010 1010 '<sup><a href="#fn\1">\1</a></sup>\2' )
1011 1011 end
1012 1012
1013 1013 OFFTAGS = /(code|pre|kbd|notextile)/
1014 1014 OFFTAG_MATCH = /(?:(<\/#{ OFFTAGS }>)|(<#{ OFFTAGS }[^>]*>))(.*?)(?=<\/?#{ OFFTAGS }|\Z)/mi
1015 1015 OFFTAG_OPEN = /<#{ OFFTAGS }/
1016 1016 OFFTAG_CLOSE = /<\/?#{ OFFTAGS }/
1017 1017 HASTAG_MATCH = /(<\/?\w[^\n]*?>)/m
1018 1018 ALLTAG_MATCH = /(<\/?\w[^\n]*?>)|.*?(?=<\/?\w[^\n]*?>|$)/m
1019 1019
1020 1020 def glyphs_textile( text, level = 0 )
1021 1021 if text !~ HASTAG_MATCH
1022 1022 pgl text
1023 1023 footnote_ref text
1024 1024 else
1025 1025 codepre = 0
1026 1026 text.gsub!( ALLTAG_MATCH ) do |line|
1027 1027 ## matches are off if we're between <code>, <pre> etc.
1028 1028 if $1
1029 1029 if line =~ OFFTAG_OPEN
1030 1030 codepre += 1
1031 1031 elsif line =~ OFFTAG_CLOSE
1032 1032 codepre -= 1
1033 1033 codepre = 0 if codepre < 0
1034 1034 end
1035 1035 elsif codepre.zero?
1036 1036 glyphs_textile( line, level + 1 )
1037 1037 else
1038 1038 htmlesc( line, :NoQuotes )
1039 1039 end
1040 1040 # p [level, codepre, line]
1041 1041
1042 1042 line
1043 1043 end
1044 1044 end
1045 1045 end
1046 1046
1047 1047 def rip_offtags( text )
1048 1048 if text =~ /<.*>/
1049 1049 ## strip and encode <pre> content
1050 1050 codepre, used_offtags = 0, {}
1051 1051 text.gsub!( OFFTAG_MATCH ) do |line|
1052 1052 if $3
1053 1053 offtag, aftertag = $4, $5
1054 1054 codepre += 1
1055 1055 used_offtags[offtag] = true
1056 1056 if codepre - used_offtags.length > 0
1057 1057 htmlesc( line, :NoQuotes )
1058 1058 @pre_list.last << line
1059 1059 line = ""
1060 1060 else
1061 1061 htmlesc( aftertag, :NoQuotes ) if aftertag
1062 1062 line = "<redpre##{ @pre_list.length }>"
1063 1063 $3.match(/<#{ OFFTAGS }([^>]*)>/)
1064 1064 tag = $1
1065 1065 $2.to_s.match(/(class\=\S+)/i)
1066 1066 tag << " #{$1}" if $1
1067 1067 @pre_list << "<#{ tag }>#{ aftertag }"
1068 1068 end
1069 1069 elsif $1 and codepre > 0
1070 1070 if codepre - used_offtags.length > 0
1071 1071 htmlesc( line, :NoQuotes )
1072 1072 @pre_list.last << line
1073 1073 line = ""
1074 1074 end
1075 1075 codepre -= 1 unless codepre.zero?
1076 1076 used_offtags = {} if codepre.zero?
1077 1077 end
1078 1078 line
1079 1079 end
1080 1080 end
1081 1081 text
1082 1082 end
1083 1083
1084 1084 def smooth_offtags( text )
1085 1085 unless @pre_list.empty?
1086 1086 ## replace <pre> content
1087 1087 text.gsub!( /<redpre#(\d+)>/ ) { @pre_list[$1.to_i] }
1088 1088 end
1089 1089 end
1090 1090
1091 1091 def inline( text )
1092 1092 [/^inline_/, /^glyphs_/].each do |meth_re|
1093 1093 @rules.each do |rule_name|
1094 1094 method( rule_name ).call( text ) if rule_name.to_s.match( meth_re )
1095 1095 end
1096 1096 end
1097 1097 end
1098 1098
1099 1099 def h_align( text )
1100 1100 H_ALGN_VALS[text]
1101 1101 end
1102 1102
1103 1103 def v_align( text )
1104 1104 V_ALGN_VALS[text]
1105 1105 end
1106 1106
1107 1107 def textile_popup_help( name, windowW, windowH )
1108 1108 ' <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 />'
1109 1109 end
1110 1110
1111 1111 # HTML cleansing stuff
1112 1112 BASIC_TAGS = {
1113 1113 'a' => ['href', 'title'],
1114 1114 'img' => ['src', 'alt', 'title'],
1115 1115 'br' => [],
1116 1116 'i' => nil,
1117 1117 'u' => nil,
1118 1118 'b' => nil,
1119 1119 'pre' => nil,
1120 1120 'kbd' => nil,
1121 1121 'code' => ['lang'],
1122 1122 'cite' => nil,
1123 1123 'strong' => nil,
1124 1124 'em' => nil,
1125 1125 'ins' => nil,
1126 1126 'sup' => nil,
1127 1127 'sub' => nil,
1128 1128 'del' => nil,
1129 1129 'table' => nil,
1130 1130 'tr' => nil,
1131 1131 'td' => ['colspan', 'rowspan'],
1132 1132 'th' => nil,
1133 1133 'ol' => nil,
1134 1134 'ul' => nil,
1135 1135 'li' => nil,
1136 1136 'p' => nil,
1137 1137 'h1' => nil,
1138 1138 'h2' => nil,
1139 1139 'h3' => nil,
1140 1140 'h4' => nil,
1141 1141 'h5' => nil,
1142 1142 'h6' => nil,
1143 1143 'blockquote' => ['cite']
1144 1144 }
1145 1145
1146 1146 def clean_html( text, tags = BASIC_TAGS )
1147 1147 text.gsub!( /<!\[CDATA\[/, '' )
1148 1148 text.gsub!( /<(\/*)(\w+)([^>]*)>/ ) do
1149 1149 raw = $~
1150 1150 tag = raw[2].downcase
1151 1151 if tags.has_key? tag
1152 1152 pcs = [tag]
1153 1153 tags[tag].each do |prop|
1154 1154 ['"', "'", ''].each do |q|
1155 1155 q2 = ( q != '' ? q : '\s' )
1156 1156 if raw[3] =~ /#{prop}\s*=\s*#{q}([^#{q2}]+)#{q}/i
1157 1157 attrv = $1
1158 1158 next if prop == 'src' and attrv =~ %r{^(?!http)\w+:}
1159 1159 pcs << "#{prop}=\"#{$1.gsub('"', '\\"')}\""
1160 1160 break
1161 1161 end
1162 1162 end
1163 1163 end if tags[tag]
1164 1164 "<#{raw[1]}#{pcs.join " "}>"
1165 1165 else
1166 1166 " "
1167 1167 end
1168 1168 end
1169 1169 end
1170 1170
1171 1171 ALLOWED_TAGS = %w(redpre pre code notextile)
1172 1172
1173 1173 def escape_html_tags(text)
1174 1174 text.gsub!(%r{<(\/?([!\w]+)[^<>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "&lt;#{$1}#{'&gt;' unless $3.blank?}" }
1175 1175 end
1176 1176 end
1177 1177
@@ -1,205 +1,205
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'redmine/scm/adapters/abstract_adapter'
19 19
20 20 module Redmine
21 21 module Scm
22 22 module Adapters
23 23 class MercurialAdapter < AbstractAdapter
24 24
25 25 # Mercurial executable name
26 26 HG_BIN = "hg"
27 27 TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial"
28 28 TEMPLATE_NAME = "hg-template"
29 29 TEMPLATE_EXTENSION = "tmpl"
30 30
31 31 class << self
32 32 def client_version
33 33 @@client_version ||= (hgversion || [])
34 34 end
35 35
36 36 def hgversion
37 37 # The hg version is expressed either as a
38 38 # release number (eg 0.9.5 or 1.0) or as a revision
39 39 # id composed of 12 hexa characters.
40 40 theversion = hgversion_from_command_line
41 41 if theversion.match(/^\d+(\.\d+)+/)
42 42 theversion.split(".").collect(&:to_i)
43 43 end
44 44 end
45 45
46 46 def hgversion_from_command_line
47 47 %x{#{HG_BIN} --version}.match(/\(version (.*)\)/)[1]
48 48 end
49 49
50 50 def template_path
51 51 @@template_path ||= template_path_for(client_version)
52 52 end
53 53
54 54 def template_path_for(version)
55 55 if ((version <=> [0,9,5]) > 0) || version.empty?
56 56 ver = "1.0"
57 57 else
58 58 ver = "0.9.5"
59 59 end
60 60 "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}"
61 61 end
62 62 end
63 63
64 64 def info
65 65 cmd = "#{HG_BIN} -R #{target('')} root"
66 66 root_url = nil
67 67 shellout(cmd) do |io|
68 68 root_url = io.gets
69 69 end
70 70 return nil if $? && $?.exitstatus != 0
71 71 info = Info.new({:root_url => root_url.chomp,
72 72 :lastrev => revisions(nil,nil,nil,{:limit => 1}).last
73 73 })
74 74 info
75 75 rescue CommandFailed
76 76 return nil
77 77 end
78 78
79 79 def entries(path=nil, identifier=nil)
80 80 path ||= ''
81 81 entries = Entries.new
82 82 cmd = "#{HG_BIN} -R #{target('')} --cwd #{target('')} locate"
83 83 cmd << " -r " + (identifier ? identifier.to_s : "tip")
84 84 cmd << " " + shell_quote("path:#{path}") unless path.empty?
85 85 shellout(cmd) do |io|
86 86 io.each_line do |line|
87 87 # HG uses antislashs as separator on Windows
88 88 line = line.gsub(/\\/, "/")
89 89 if path.empty? or e = line.gsub!(%r{^#{with_trailling_slash(path)}},'')
90 90 e ||= line
91 91 e = e.chomp.split(%r{[\/\\]})
92 92 entries << Entry.new({:name => e.first,
93 93 :path => (path.nil? or path.empty? ? e.first : "#{with_trailling_slash(path)}#{e.first}"),
94 94 :kind => (e.size > 1 ? 'dir' : 'file'),
95 95 :lastrev => Revision.new
96 96 }) unless entries.detect{|entry| entry.name == e.first}
97 97 end
98 98 end
99 99 end
100 100 return nil if $? && $?.exitstatus != 0
101 101 entries.sort_by_name
102 102 end
103 103
104 104 # Fetch the revisions by using a template file that
105 105 # makes Mercurial produce a xml output.
106 106 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
107 107 revisions = Revisions.new
108 cmd = "#{HG_BIN} --debug --encoding utf8 -R #{target('')} log -C --style #{self.class.template_path}"
108 cmd = "#{HG_BIN} --debug --encoding utf8 -R #{target('')} log -C --style #{shell_quote self.class.template_path}"
109 109 if identifier_from && identifier_to
110 110 cmd << " -r #{identifier_from.to_i}:#{identifier_to.to_i}"
111 111 elsif identifier_from
112 112 cmd << " -r #{identifier_from.to_i}:"
113 113 end
114 114 cmd << " --limit #{options[:limit].to_i}" if options[:limit]
115 115 cmd << " #{path}" if path
116 116 shellout(cmd) do |io|
117 117 begin
118 118 # HG doesn't close the XML Document...
119 119 doc = REXML::Document.new(io.read << "</log>")
120 120 doc.elements.each("log/logentry") do |logentry|
121 121 paths = []
122 122 copies = logentry.get_elements('paths/path-copied')
123 123 logentry.elements.each("paths/path") do |path|
124 124 # Detect if the added file is a copy
125 125 if path.attributes['action'] == 'A' and c = copies.find{ |e| e.text == path.text }
126 126 from_path = c.attributes['copyfrom-path']
127 127 from_rev = logentry.attributes['revision']
128 128 end
129 129 paths << {:action => path.attributes['action'],
130 130 :path => "/#{path.text}",
131 131 :from_path => from_path ? "/#{from_path}" : nil,
132 132 :from_revision => from_rev ? from_rev : nil
133 133 }
134 134 end
135 135 paths.sort! { |x,y| x[:path] <=> y[:path] }
136 136
137 137 revisions << Revision.new({:identifier => logentry.attributes['revision'],
138 138 :scmid => logentry.attributes['node'],
139 139 :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),
140 140 :time => Time.parse(logentry.elements['date'].text).localtime,
141 141 :message => logentry.elements['msg'].text,
142 142 :paths => paths
143 143 })
144 144 end
145 145 rescue
146 146 logger.debug($!)
147 147 end
148 148 end
149 149 return nil if $? && $?.exitstatus != 0
150 150 revisions
151 151 end
152 152
153 153 def diff(path, identifier_from, identifier_to=nil)
154 154 path ||= ''
155 155 if identifier_to
156 156 identifier_to = identifier_to.to_i
157 157 else
158 158 identifier_to = identifier_from.to_i - 1
159 159 end
160 160 cmd = "#{HG_BIN} -R #{target('')} diff -r #{identifier_to} -r #{identifier_from} --nodates"
161 161 cmd << " -I #{target(path)}" unless path.empty?
162 162 diff = []
163 163 shellout(cmd) do |io|
164 164 io.each_line do |line|
165 165 diff << line
166 166 end
167 167 end
168 168 return nil if $? && $?.exitstatus != 0
169 169 diff
170 170 end
171 171
172 172 def cat(path, identifier=nil)
173 173 cmd = "#{HG_BIN} -R #{target('')} cat"
174 174 cmd << " -r " + (identifier ? identifier.to_s : "tip")
175 175 cmd << " #{target(path)}"
176 176 cat = nil
177 177 shellout(cmd) do |io|
178 178 io.binmode
179 179 cat = io.read
180 180 end
181 181 return nil if $? && $?.exitstatus != 0
182 182 cat
183 183 end
184 184
185 185 def annotate(path, identifier=nil)
186 186 path ||= ''
187 187 cmd = "#{HG_BIN} -R #{target('')}"
188 188 cmd << " annotate -n -u"
189 189 cmd << " -r " + (identifier ? identifier.to_s : "tip")
190 190 cmd << " -r #{identifier.to_i}" if identifier
191 191 cmd << " #{target(path)}"
192 192 blame = Annotate.new
193 193 shellout(cmd) do |io|
194 194 io.each_line do |line|
195 195 next unless line =~ %r{^([^:]+)\s(\d+):(.*)$}
196 196 blame.add_line($3.rstrip, Revision.new(:identifier => $2.to_i, :author => $1.strip))
197 197 end
198 198 end
199 199 return nil if $? && $?.exitstatus != 0
200 200 blame
201 201 end
202 202 end
203 203 end
204 204 end
205 205 end
@@ -1,37 +1,46
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class AttachmentTest < Test::Unit::TestCase
21 21 fixtures :issues, :users
22 22
23 23 def setup
24 24 end
25
26 def test_create
27 a = Attachment.new(:container => Issue.find(1),
28 :file => test_uploaded_file("testfile.txt", "text/plain"),
29 :author => User.find(1))
30 assert a.save
31 assert_equal 'testfile.txt', a.filename
32 assert_equal 59, a.filesize
33 assert_equal 'text/plain', a.content_type
34 assert_equal 0, a.downloads
35 assert_equal Digest::MD5.hexdigest(test_uploaded_file("testfile.txt", "text/plain").read), a.digest
36 assert File.exist?(a.diskfile)
37 end
25 38
26 39 def test_diskfilename
27 40 assert Attachment.disk_filename("test_file.txt") =~ /^\d{12}_test_file.txt$/
28 41 assert_equal 'test_file.txt', Attachment.disk_filename("test_file.txt")[13..-1]
29 42 assert_equal '770c509475505f37c2b8fb6030434d6b.txt', Attachment.disk_filename("test_accentuΓ©.txt")[13..-1]
30 43 assert_equal 'f8139524ebb8f32e51976982cd20a85d', Attachment.disk_filename("test_accentuΓ©")[13..-1]
31 44 assert_equal 'cbb5b0f30978ba03731d61f9f6d10011', Attachment.disk_filename("test_accentuΓ©.Γ§a")[13..-1]
32 45 end
33
34 def test_digest
35 assert_equal '1478adae0d4eb06d35897518540e25d6', Attachment.digest(Test::Unit::TestCase.fixture_path + "/files/testfile.txt")
36 end
37 46 end
@@ -1,449 +1,455
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../../test_helper'
19 19
20 20 class ApplicationHelperTest < HelperTestCase
21 21 include ApplicationHelper
22 22 include ActionView::Helpers::TextHelper
23 23 fixtures :projects, :roles, :enabled_modules, :users,
24 24 :repositories, :changesets,
25 25 :trackers, :issue_statuses, :issues, :versions, :documents,
26 26 :wikis, :wiki_pages, :wiki_contents,
27 27 :boards, :messages,
28 28 :attachments
29 29
30 30 def setup
31 31 super
32 32 end
33 33
34 34 def test_auto_links
35 35 to_test = {
36 36 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
37 37 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
38 38 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
39 39 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
40 40 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
41 41 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
42 42 '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>.',
43 43 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
44 44 '(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>)',
45 45 '(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>)',
46 46 '(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>).',
47 47 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
48 48 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
49 49 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
50 50 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
51 51 '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>',
52 52 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
53 53 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
54 54 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
55 55 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
56 56 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
57 57 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
58 # two exclamation marks
59 '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>',
58 60 }
59 61 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
60 62 end
61 63
62 64 def test_auto_mailto
63 65 assert_equal '<p><a href="mailto:test@foo.bar" class="email">test@foo.bar</a></p>',
64 66 textilizable('test@foo.bar')
65 67 end
66 68
67 69 def test_inline_images
68 70 to_test = {
69 71 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
70 72 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
71 73 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
72 74 # inline styles should be stripped
73 75 'with style !{width:100px;height100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" alt="" />',
74 76 '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" />',
75 77 '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;" />',
76 78 }
77 79 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
78 80 end
79 81
80 82 def test_acronyms
81 83 to_test = {
82 84 'this is an acronym: GPL(General Public License)' => 'this is an acronym: <acronym title="General Public License">GPL</acronym>',
83 85 'GPL(This is a double-quoted "title")' => '<acronym title="This is a double-quoted &quot;title&quot;">GPL</acronym>',
84 86 }
85 87 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
86 88
87 89 end
88 90
89 91 def test_attached_images
90 92 to_test = {
91 93 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
92 94 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
93 95 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
94 96 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />'
95 97 }
96 98 attachments = Attachment.find(:all)
97 99 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
98 100 end
99 101
100 102 def test_textile_external_links
101 103 to_test = {
102 104 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
103 105 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
104 106 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
105 107 '"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>',
106 108 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
107 109 # no multiline link text
108 "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 />\nand another on a second line\":test"
110 "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 />\nand another on a second line\":test",
111 # mailto link
112 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
113 # two exclamation marks
114 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
109 115 }
110 116 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
111 117 end
112 118
113 119 def test_redmine_links
114 120 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
115 121 :class => 'issue', :title => 'Error 281 when updating a recipe (New)')
116 122
117 123 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
118 124 :class => 'changeset', :title => 'My very first commit')
119 125
120 126 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
121 127 :class => 'document')
122 128
123 129 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
124 130 :class => 'version')
125 131
126 132 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
127 133
128 134 source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}
129 135 source_url_with_ext = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file.ext']}
130 136
131 137 to_test = {
132 138 # tickets
133 139 '#3, #3 and #3.' => "#{issue_link}, #{issue_link} and #{issue_link}.",
134 140 # changesets
135 141 'r1' => changeset_link,
136 142 # documents
137 143 'document#1' => document_link,
138 144 'document:"Test document"' => document_link,
139 145 # versions
140 146 'version#2' => version_link,
141 147 'version:1.0' => version_link,
142 148 'version:"1.0"' => version_link,
143 149 # source
144 150 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
145 151 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
146 152 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
147 153 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
148 154 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
149 155 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
150 156 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
151 157 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'),
152 158 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
153 159 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'),
154 160 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
155 161 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
156 162 # message
157 163 'message#4' => link_to('Post 2', message_url, :class => 'message'),
158 164 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5'), :class => 'message'),
159 165 # escaping
160 166 '!#3.' => '#3.',
161 167 '!r1' => 'r1',
162 168 '!document#1' => 'document#1',
163 169 '!document:"Test document"' => 'document:"Test document"',
164 170 '!version#2' => 'version#2',
165 171 '!version:1.0' => 'version:1.0',
166 172 '!version:"1.0"' => 'version:"1.0"',
167 173 '!source:/some/file' => 'source:/some/file',
168 174 # invalid expressions
169 175 'source:' => 'source:',
170 176 # url hash
171 177 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
172 178 }
173 179 @project = Project.find(1)
174 180 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
175 181 end
176 182
177 183 def test_wiki_links
178 184 to_test = {
179 185 '[[CookBook documentation]]' => '<a href="/wiki/ecookbook/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
180 186 '[[Another page|Page]]' => '<a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a>',
181 187 # link with anchor
182 188 '[[CookBook documentation#One-section]]' => '<a href="/wiki/ecookbook/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
183 189 '[[Another page#anchor|Page]]' => '<a href="/wiki/ecookbook/Another_page#anchor" class="wiki-page">Page</a>',
184 190 # page that doesn't exist
185 191 '[[Unknown page]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">Unknown page</a>',
186 192 '[[Unknown page|404]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">404</a>',
187 193 # link to another project wiki
188 194 '[[onlinestore:]]' => '<a href="/wiki/onlinestore/" class="wiki-page">onlinestore</a>',
189 195 '[[onlinestore:|Wiki]]' => '<a href="/wiki/onlinestore/" class="wiki-page">Wiki</a>',
190 196 '[[onlinestore:Start page]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Start page</a>',
191 197 '[[onlinestore:Start page|Text]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Text</a>',
192 198 '[[onlinestore:Unknown page]]' => '<a href="/wiki/onlinestore/Unknown_page" class="wiki-page new">Unknown page</a>',
193 199 # striked through link
194 200 '-[[Another page|Page]]-' => '<del><a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a></del>',
195 201 '-[[Another page|Page]] link-' => '<del><a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a> link</del>',
196 202 # escaping
197 203 '![[Another page|Page]]' => '[[Another page|Page]]',
198 204 }
199 205 @project = Project.find(1)
200 206 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
201 207 end
202 208
203 209 def test_html_tags
204 210 to_test = {
205 211 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
206 212 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
207 213 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
208 214 # do not escape pre/code tags
209 215 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
210 216 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
211 217 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
212 218 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
213 219 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
214 220 # remove attributes except class
215 221 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
216 222 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
217 223 }
218 224 to_test.each { |text, result| assert_equal result, textilizable(text) }
219 225 end
220 226
221 227 def test_allowed_html_tags
222 228 to_test = {
223 229 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
224 230 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
225 231 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
226 232 }
227 233 to_test.each { |text, result| assert_equal result, textilizable(text) }
228 234 end
229 235
230 236 def syntax_highlight
231 237 raw = <<-RAW
232 238 <pre><code class="ruby">
233 239 # Some ruby code here
234 240 </pre></code>
235 241 RAW
236 242
237 243 expected = <<-EXPECTED
238 244 <pre><code class="ruby CodeRay"><span class="no">1</span> <span class="c"># Some ruby code here</span>
239 245 </pre></code>
240 246 EXPECTED
241 247
242 248 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
243 249 end
244 250
245 251 def test_wiki_links_in_tables
246 252 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
247 253 '<tr><td><a href="/wiki/ecookbook/Page" class="wiki-page new">Link title</a></td>' +
248 254 '<td><a href="/wiki/ecookbook/Other_Page" class="wiki-page new">Other title</a></td>' +
249 255 '</tr><tr><td>Cell 21</td><td><a href="/wiki/ecookbook/Last_page" class="wiki-page new">Last page</a></td></tr>'
250 256 }
251 257 @project = Project.find(1)
252 258 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
253 259 end
254 260
255 261 def test_text_formatting
256 262 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
257 263 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
258 264 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
259 265 '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>',
260 266 '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',
261 267 }
262 268 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
263 269 end
264 270
265 271 def test_wiki_horizontal_rule
266 272 assert_equal '<hr />', textilizable('---')
267 273 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
268 274 end
269 275
270 276 def test_acronym
271 277 assert_equal '<p>This is an acronym: <acronym title="American Civil Liberties Union">ACLU</acronym>.</p>',
272 278 textilizable('This is an acronym: ACLU(American Civil Liberties Union).')
273 279 end
274 280
275 281 def test_footnotes
276 282 raw = <<-RAW
277 283 This is some text[1].
278 284
279 285 fn1. This is the foot note
280 286 RAW
281 287
282 288 expected = <<-EXPECTED
283 289 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
284 290 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
285 291 EXPECTED
286 292
287 293 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
288 294 end
289 295
290 296 def test_table_of_content
291 297 raw = <<-RAW
292 298 {{toc}}
293 299
294 300 h1. Title
295 301
296 302 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
297 303
298 304 h2. Subtitle
299 305
300 306 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
301 307
302 308 h2. Subtitle with %{color:red}red text%
303 309
304 310 h1. Another title
305 311
306 312 RAW
307 313
308 314 expected = '<ul class="toc">' +
309 315 '<li class="heading1"><a href="#Title">Title</a></li>' +
310 316 '<li class="heading2"><a href="#Subtitle">Subtitle</a></li>' +
311 317 '<li class="heading2"><a href="#Subtitle-with-red-text">Subtitle with red text</a></li>' +
312 318 '<li class="heading1"><a href="#Another-title">Another title</a></li>' +
313 319 '</ul>'
314 320
315 321 assert textilizable(raw).gsub("\n", "").include?(expected)
316 322 end
317 323
318 324 def test_blockquote
319 325 # orig raw text
320 326 raw = <<-RAW
321 327 John said:
322 328 > Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
323 329 > Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
324 330 > * Donec odio lorem,
325 331 > * sagittis ac,
326 332 > * malesuada in,
327 333 > * adipiscing eu, dolor.
328 334 >
329 335 > >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
330 336 > Proin a tellus. Nam vel neque.
331 337
332 338 He's right.
333 339 RAW
334 340
335 341 # expected html
336 342 expected = <<-EXPECTED
337 343 <p>John said:</p>
338 344 <blockquote>
339 345 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
340 346 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
341 347 <ul>
342 348 <li>Donec odio lorem,</li>
343 349 <li>sagittis ac,</li>
344 350 <li>malesuada in,</li>
345 351 <li>adipiscing eu, dolor.</li>
346 352 </ul>
347 353 <blockquote>
348 354 <p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
349 355 </blockquote>
350 356 <p>Proin a tellus. Nam vel neque.</p>
351 357 </blockquote>
352 358 <p>He's right.</p>
353 359 EXPECTED
354 360
355 361 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
356 362 end
357 363
358 364 def test_table
359 365 raw = <<-RAW
360 366 This is a table with empty cells:
361 367
362 368 |cell11|cell12||
363 369 |cell21||cell23|
364 370 |cell31|cell32|cell33|
365 371 RAW
366 372
367 373 expected = <<-EXPECTED
368 374 <p>This is a table with empty cells:</p>
369 375
370 376 <table>
371 377 <tr><td>cell11</td><td>cell12</td><td></td></tr>
372 378 <tr><td>cell21</td><td></td><td>cell23</td></tr>
373 379 <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
374 380 </table>
375 381 EXPECTED
376 382
377 383 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
378 384 end
379 385
380 386 def test_default_formatter
381 387 Setting.text_formatting = 'unknown'
382 388 text = 'a *link*: http://www.example.net/'
383 389 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
384 390 Setting.text_formatting = 'textile'
385 391 end
386 392
387 393 def test_date_format_default
388 394 today = Date.today
389 395 Setting.date_format = ''
390 396 assert_equal l_date(today), format_date(today)
391 397 end
392 398
393 399 def test_date_format
394 400 today = Date.today
395 401 Setting.date_format = '%d %m %Y'
396 402 assert_equal today.strftime('%d %m %Y'), format_date(today)
397 403 end
398 404
399 405 def test_time_format_default
400 406 now = Time.now
401 407 Setting.date_format = ''
402 408 Setting.time_format = ''
403 409 assert_equal l_datetime(now), format_time(now)
404 410 assert_equal l_time(now), format_time(now, false)
405 411 end
406 412
407 413 def test_time_format
408 414 now = Time.now
409 415 Setting.date_format = '%d %m %Y'
410 416 Setting.time_format = '%H %M'
411 417 assert_equal now.strftime('%d %m %Y %H %M'), format_time(now)
412 418 assert_equal now.strftime('%H %M'), format_time(now, false)
413 419 end
414 420
415 421 def test_utc_time_format
416 422 now = Time.now.utc
417 423 Setting.date_format = '%d %m %Y'
418 424 Setting.time_format = '%H %M'
419 425 assert_equal Time.now.strftime('%d %m %Y %H %M'), format_time(now)
420 426 assert_equal Time.now.strftime('%H %M'), format_time(now, false)
421 427 end
422 428
423 429 def test_due_date_distance_in_words
424 430 to_test = { Date.today => 'Due in 0 days',
425 431 Date.today + 1 => 'Due in 1 day',
426 432 Date.today + 100 => 'Due in 100 days',
427 433 Date.today + 20000 => 'Due in 20000 days',
428 434 Date.today - 1 => '1 day late',
429 435 Date.today - 100 => '100 days late',
430 436 Date.today - 20000 => '20000 days late',
431 437 }
432 438 to_test.each do |date, expected|
433 439 assert_equal expected, due_date_distance_in_words(date)
434 440 end
435 441 end
436 442
437 443 def test_avatar
438 444 # turn on avatars
439 445 Setting.gravatar_enabled = '1'
440 446 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
441 447 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
442 448 assert_nil avatar('jsmith')
443 449 assert_nil avatar(nil)
444 450
445 451 # turn off avatars
446 452 Setting.gravatar_enabled = '0'
447 453 assert_nil avatar(User.find_by_mail('jsmith@somenet.foo'))
448 454 end
449 455 end
General Comments 0
You need to be logged in to leave comments. Login now