##// END OF EJS Templates
Makes wiki text formatter pluggable....
Jean-Philippe Lang -
r1953:a3b9a5aa5fe3
parent child
Show More
@@ -0,0 +1,183
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require 'redcloth3'
19 require 'coderay'
20
21 module Redmine
22 module WikiFormatting
23 module Textile
24 class Formatter < RedCloth3
25
26 # auto_link rule after textile rules so that it doesn't break !image_url! tags
27 RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto, :inline_toc, :inline_macros]
28
29 def initialize(*args)
30 super
31 self.hard_breaks=true
32 self.no_span_caps=true
33 end
34
35 def to_html(*rules, &block)
36 @toc = []
37 @macros_runner = block
38 super(*RULES).to_s
39 end
40
41 private
42
43 # Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet.
44 # <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a>
45 def hard_break( text )
46 text.gsub!( /(.)\n(?!\n|\Z|>| *(>? *[#*=]+(\s|$)|[{|]))/, "\\1<br />\n" ) if hard_breaks
47 end
48
49 # Patch to add code highlighting support to RedCloth
50 def smooth_offtags( text )
51 unless @pre_list.empty?
52 ## replace <pre> content
53 text.gsub!(/<redpre#(\d+)>/) do
54 content = @pre_list[$1.to_i]
55 if content.match(/<code\s+class="(\w+)">\s?(.+)/m)
56 content = "<code class=\"#{$1} CodeRay\">" +
57 CodeRay.scan($2, $1.downcase).html(:escape => false, :line_numbers => :inline)
58 end
59 content
60 end
61 end
62 end
63
64 # Patch to add 'table of content' support to RedCloth
65 def textile_p_withtoc(tag, atts, cite, content)
66 # removes wiki links from the item
67 toc_item = content.gsub(/(\[\[|\]\])/, '')
68 # removes styles
69 # eg. %{color:red}Triggers% => Triggers
70 toc_item.gsub! %r[%\{[^\}]*\}([^%]+)%], '\\1'
71
72 # replaces non word caracters by dashes
73 anchor = toc_item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
74
75 unless anchor.blank?
76 if tag =~ /^h(\d)$/
77 @toc << [$1.to_i, anchor, toc_item]
78 end
79 atts << " id=\"#{anchor}\""
80 content = content + "<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a>"
81 end
82 textile_p(tag, atts, cite, content)
83 end
84
85 alias :textile_h1 :textile_p_withtoc
86 alias :textile_h2 :textile_p_withtoc
87 alias :textile_h3 :textile_p_withtoc
88
89 def inline_toc(text)
90 text.gsub!(/<p>\{\{([<>]?)toc\}\}<\/p>/i) do
91 div_class = 'toc'
92 div_class << ' right' if $1 == '>'
93 div_class << ' left' if $1 == '<'
94 out = "<ul class=\"#{div_class}\">"
95 @toc.each do |heading|
96 level, anchor, toc_item = heading
97 out << "<li class=\"heading#{level}\"><a href=\"##{anchor}\">#{toc_item}</a></li>\n"
98 end
99 out << '</ul>'
100 out
101 end
102 end
103
104 MACROS_RE = /
105 (!)? # escaping
106 (
107 \{\{ # opening tag
108 ([\w]+) # macro name
109 (\(([^\}]*)\))? # optional arguments
110 \}\} # closing tag
111 )
112 /x unless const_defined?(:MACROS_RE)
113
114 def inline_macros(text)
115 text.gsub!(MACROS_RE) do
116 esc, all, macro = $1, $2, $3.downcase
117 args = ($5 || '').split(',').each(&:strip)
118 if esc.nil?
119 begin
120 @macros_runner.call(macro, args)
121 rescue => e
122 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
123 end || all
124 else
125 all
126 end
127 end
128 end
129
130 AUTO_LINK_RE = %r{
131 ( # leading text
132 <\w+.*?>| # leading HTML tag, or
133 [^=<>!:'"/]| # leading punctuation, or
134 ^ # beginning of line
135 )
136 (
137 (?:https?://)| # protocol spec, or
138 (?:ftp://)|
139 (?:www\.) # www.*
140 )
141 (
142 (\S+?) # url
143 (\/)? # slash
144 )
145 ([^\w\=\/;\(\)]*?) # post
146 (?=<|\s|$)
147 }x unless const_defined?(:AUTO_LINK_RE)
148
149 # Turns all urls into clickable links (code from Rails).
150 def inline_auto_link(text)
151 text.gsub!(AUTO_LINK_RE) do
152 all, leading, proto, url, post = $&, $1, $2, $3, $6
153 if leading =~ /<a\s/i || leading =~ /![<>=]?/
154 # don't replace URL's that are already linked
155 # and URL's prefixed with ! !> !< != (textile images)
156 all
157 else
158 # Idea below : an URL with unbalanced parethesis and
159 # ending by ')' is put into external parenthesis
160 if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
161 url=url[0..-2] # discard closing parenth from url
162 post = ")"+post # add closing parenth to post
163 end
164 %(#{leading}<a class="external" href="#{proto=="www."?"http://www.":proto}#{url}">#{proto + url}</a>#{post})
165 end
166 end
167 end
168
169 # Turns all email addresses into clickable links (code from Rails).
170 def inline_auto_mailto(text)
171 text.gsub!(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
172 mail = $1
173 if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/)
174 mail
175 else
176 %{<a href="mailto:#{mail}" class="email">#{mail}</a>}
177 end
178 end
179 end
180 end
181 end
182 end
183 end
@@ -0,0 +1,43
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 module Redmine
19 module WikiFormatting
20 module Textile
21 module Helper
22 def wikitoolbar_for(field_id)
23 help_link = l(:setting_text_formatting) + ': ' +
24 link_to(l(:label_help), compute_public_path('wiki_syntax', 'help', 'html'),
25 :onclick => "window.open(\"#{ compute_public_path('wiki_syntax', 'help', 'html') }\", \"\", \"resizable=yes, location=no, width=300, height=640, menubar=no, status=no, scrollbars=yes\"); return false;")
26
27 javascript_include_tag('jstoolbar/jstoolbar') +
28 javascript_include_tag('jstoolbar/textile') +
29 javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language}") +
30 javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.setHelpLink('#{help_link}'); toolbar.draw();")
31 end
32
33 def initial_page_content(page)
34 "h1. #{@page.pretty_title}"
35 end
36
37 def heads_for_wiki_formatter
38 stylesheet_link_tag 'jstoolbar'
39 end
40 end
41 end
42 end
43 end
@@ -0,0 +1,200
1 /* ***** BEGIN LICENSE BLOCK *****
2 * This file is part of DotClear.
3 * Copyright (c) 2005 Nicolas Martin & Olivier Meunier and contributors. All
4 * rights reserved.
5 *
6 * DotClear is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * DotClear is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with DotClear; if not, write to the Free Software
18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 *
20 * ***** END LICENSE BLOCK *****
21 */
22
23 /* Modified by JP LANG for textile formatting */
24
25 // strong
26 jsToolBar.prototype.elements.strong = {
27 type: 'button',
28 title: 'Strong',
29 fn: {
30 wiki: function() { this.singleTag('*') }
31 }
32 }
33
34 // em
35 jsToolBar.prototype.elements.em = {
36 type: 'button',
37 title: 'Italic',
38 fn: {
39 wiki: function() { this.singleTag("_") }
40 }
41 }
42
43 // ins
44 jsToolBar.prototype.elements.ins = {
45 type: 'button',
46 title: 'Underline',
47 fn: {
48 wiki: function() { this.singleTag('+') }
49 }
50 }
51
52 // del
53 jsToolBar.prototype.elements.del = {
54 type: 'button',
55 title: 'Deleted',
56 fn: {
57 wiki: function() { this.singleTag('-') }
58 }
59 }
60
61 // code
62 jsToolBar.prototype.elements.code = {
63 type: 'button',
64 title: 'Code',
65 fn: {
66 wiki: function() { this.singleTag('@') }
67 }
68 }
69
70 // spacer
71 jsToolBar.prototype.elements.space1 = {type: 'space'}
72
73 // headings
74 jsToolBar.prototype.elements.h1 = {
75 type: 'button',
76 title: 'Heading 1',
77 fn: {
78 wiki: function() {
79 this.encloseLineSelection('h1. ', '',function(str) {
80 str = str.replace(/^h\d+\.\s+/, '')
81 return str;
82 });
83 }
84 }
85 }
86 jsToolBar.prototype.elements.h2 = {
87 type: 'button',
88 title: 'Heading 2',
89 fn: {
90 wiki: function() {
91 this.encloseLineSelection('h2. ', '',function(str) {
92 str = str.replace(/^h\d+\.\s+/, '')
93 return str;
94 });
95 }
96 }
97 }
98 jsToolBar.prototype.elements.h3 = {
99 type: 'button',
100 title: 'Heading 3',
101 fn: {
102 wiki: function() {
103 this.encloseLineSelection('h3. ', '',function(str) {
104 str = str.replace(/^h\d+\.\s+/, '')
105 return str;
106 });
107 }
108 }
109 }
110
111 // spacer
112 jsToolBar.prototype.elements.space2 = {type: 'space'}
113
114 // ul
115 jsToolBar.prototype.elements.ul = {
116 type: 'button',
117 title: 'Unordered list',
118 fn: {
119 wiki: function() {
120 this.encloseLineSelection('','',function(str) {
121 str = str.replace(/\r/g,'');
122 return str.replace(/(\n|^)[#-]?\s*/g,"$1* ");
123 });
124 }
125 }
126 }
127
128 // ol
129 jsToolBar.prototype.elements.ol = {
130 type: 'button',
131 title: 'Ordered list',
132 fn: {
133 wiki: function() {
134 this.encloseLineSelection('','',function(str) {
135 str = str.replace(/\r/g,'');
136 return str.replace(/(\n|^)[*-]?\s*/g,"$1# ");
137 });
138 }
139 }
140 }
141
142 // spacer
143 jsToolBar.prototype.elements.space3 = {type: 'space'}
144
145 // bq
146 jsToolBar.prototype.elements.bq = {
147 type: 'button',
148 title: 'Quote',
149 fn: {
150 wiki: function() {
151 this.encloseLineSelection('','',function(str) {
152 str = str.replace(/\r/g,'');
153 return str.replace(/(\n|^) *([^\n]*)/g,"$1> $2");
154 });
155 }
156 }
157 }
158
159 // unbq
160 jsToolBar.prototype.elements.unbq = {
161 type: 'button',
162 title: 'Unquote',
163 fn: {
164 wiki: function() {
165 this.encloseLineSelection('','',function(str) {
166 str = str.replace(/\r/g,'');
167 return str.replace(/(\n|^) *[>]? *([^\n]*)/g,"$1$2");
168 });
169 }
170 }
171 }
172
173 // pre
174 jsToolBar.prototype.elements.pre = {
175 type: 'button',
176 title: 'Preformatted text',
177 fn: {
178 wiki: function() { this.encloseLineSelection('<pre>\n', '\n</pre>') }
179 }
180 }
181
182 // spacer
183 jsToolBar.prototype.elements.space4 = {type: 'space'}
184
185 // wiki page
186 jsToolBar.prototype.elements.link = {
187 type: 'button',
188 title: 'Wiki link',
189 fn: {
190 wiki: function() { this.encloseSelection("[[", "]]") }
191 }
192 }
193 // image
194 jsToolBar.prototype.elements.img = {
195 type: 'button',
196 title: 'Image',
197 fn: {
198 wiki: function() { this.encloseSelection("!", "!") }
199 }
200 }
@@ -1,211 +1,218
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 'diff'
19 19
20 20 class WikiController < ApplicationController
21 21 before_filter :find_wiki, :authorize
22 22
23 23 verify :method => :post, :only => [:destroy, :destroy_attachment, :protect], :redirect_to => { :action => :index }
24 24
25 25 helper :attachments
26 26 include AttachmentsHelper
27 27
28 28 # display a page (in editing mode if it doesn't exist)
29 29 def index
30 30 page_title = params[:page]
31 31 @page = @wiki.find_or_new_page(page_title)
32 32 if @page.new_record?
33 33 if User.current.allowed_to?(:edit_wiki_pages, @project)
34 34 edit
35 35 render :action => 'edit'
36 36 else
37 37 render_404
38 38 end
39 39 return
40 40 end
41 41 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
42 42 # Redirects user to the current version if he's not allowed to view previous versions
43 43 redirect_to :version => nil
44 44 return
45 45 end
46 46 @content = @page.content_for_version(params[:version])
47 47 if params[:export] == 'html'
48 48 export = render_to_string :action => 'export', :layout => false
49 49 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
50 50 return
51 51 elsif params[:export] == 'txt'
52 52 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
53 53 return
54 54 end
55 55 @editable = editable?
56 56 render :action => 'show'
57 57 end
58 58
59 59 # edit an existing page or a new one
60 60 def edit
61 61 @page = @wiki.find_or_new_page(params[:page])
62 62 return render_403 unless editable?
63 63 @page.content = WikiContent.new(:page => @page) if @page.new_record?
64 64
65 65 @content = @page.content_for_version(params[:version])
66 @content.text = "h1. #{@page.pretty_title}" if @content.text.blank?
66 @content.text = initial_page_content(@page) if @content.text.blank?
67 67 # don't keep previous comment
68 68 @content.comments = nil
69 69 if request.get?
70 70 # To prevent StaleObjectError exception when reverting to a previous version
71 71 @content.version = @page.content.version
72 72 else
73 73 if !@page.new_record? && @content.text == params[:content][:text]
74 74 # don't save if text wasn't changed
75 75 redirect_to :action => 'index', :id => @project, :page => @page.title
76 76 return
77 77 end
78 78 #@content.text = params[:content][:text]
79 79 #@content.comments = params[:content][:comments]
80 80 @content.attributes = params[:content]
81 81 @content.author = User.current
82 82 # if page is new @page.save will also save content, but not if page isn't a new record
83 83 if (@page.new_record? ? @page.save : @content.save)
84 84 redirect_to :action => 'index', :id => @project, :page => @page.title
85 85 end
86 86 end
87 87 rescue ActiveRecord::StaleObjectError
88 88 # Optimistic locking exception
89 89 flash[:error] = l(:notice_locking_conflict)
90 90 end
91 91
92 92 # rename a page
93 93 def rename
94 94 @page = @wiki.find_page(params[:page])
95 95 return render_403 unless editable?
96 96 @page.redirect_existing_links = true
97 97 # used to display the *original* title if some AR validation errors occur
98 98 @original_title = @page.pretty_title
99 99 if request.post? && @page.update_attributes(params[:wiki_page])
100 100 flash[:notice] = l(:notice_successful_update)
101 101 redirect_to :action => 'index', :id => @project, :page => @page.title
102 102 end
103 103 end
104 104
105 105 def protect
106 106 page = @wiki.find_page(params[:page])
107 107 page.update_attribute :protected, params[:protected]
108 108 redirect_to :action => 'index', :id => @project, :page => page.title
109 109 end
110 110
111 111 # show page history
112 112 def history
113 113 @page = @wiki.find_page(params[:page])
114 114
115 115 @version_count = @page.content.versions.count
116 116 @version_pages = Paginator.new self, @version_count, per_page_option, params['p']
117 117 # don't load text
118 118 @versions = @page.content.versions.find :all,
119 119 :select => "id, author_id, comments, updated_on, version",
120 120 :order => 'version DESC',
121 121 :limit => @version_pages.items_per_page + 1,
122 122 :offset => @version_pages.current.offset
123 123
124 124 render :layout => false if request.xhr?
125 125 end
126 126
127 127 def diff
128 128 @page = @wiki.find_page(params[:page])
129 129 @diff = @page.diff(params[:version], params[:version_from])
130 130 render_404 unless @diff
131 131 end
132 132
133 133 def annotate
134 134 @page = @wiki.find_page(params[:page])
135 135 @annotate = @page.annotate(params[:version])
136 136 end
137 137
138 138 # remove a wiki page and its history
139 139 def destroy
140 140 @page = @wiki.find_page(params[:page])
141 141 return render_403 unless editable?
142 142 @page.destroy if @page
143 143 redirect_to :action => 'special', :id => @project, :page => 'Page_index'
144 144 end
145 145
146 146 # display special pages
147 147 def special
148 148 page_title = params[:page].downcase
149 149 case page_title
150 150 # show pages index, sorted by title
151 151 when 'page_index', 'date_index'
152 152 # eager load information about last updates, without loading text
153 153 @pages = @wiki.pages.find :all, :select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on",
154 154 :joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id",
155 155 :order => 'title'
156 156 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
157 157 @pages_by_parent_id = @pages.group_by(&:parent_id)
158 158 # export wiki to a single html file
159 159 when 'export'
160 160 @pages = @wiki.pages.find :all, :order => 'title'
161 161 export = render_to_string :action => 'export_multiple', :layout => false
162 162 send_data(export, :type => 'text/html', :filename => "wiki.html")
163 163 return
164 164 else
165 165 # requested special page doesn't exist, redirect to default page
166 166 redirect_to :action => 'index', :id => @project, :page => nil and return
167 167 end
168 168 render :action => "special_#{page_title}"
169 169 end
170 170
171 171 def preview
172 172 page = @wiki.find_page(params[:page])
173 173 # page is nil when previewing a new page
174 174 return render_403 unless page.nil? || editable?(page)
175 175 if page
176 176 @attachements = page.attachments
177 177 @previewed = page.content
178 178 end
179 179 @text = params[:content][:text]
180 180 render :partial => 'common/preview'
181 181 end
182 182
183 183 def add_attachment
184 184 @page = @wiki.find_page(params[:page])
185 185 return render_403 unless editable?
186 186 attach_files(@page, params[:attachments])
187 187 redirect_to :action => 'index', :page => @page.title
188 188 end
189 189
190 190 def destroy_attachment
191 191 @page = @wiki.find_page(params[:page])
192 192 return render_403 unless editable?
193 193 @page.attachments.find(params[:attachment_id]).destroy
194 194 redirect_to :action => 'index', :page => @page.title
195 195 end
196 196
197 197 private
198 198
199 199 def find_wiki
200 200 @project = Project.find(params[:id])
201 201 @wiki = @project.wiki
202 202 render_404 unless @wiki
203 203 rescue ActiveRecord::RecordNotFound
204 204 render_404
205 205 end
206 206
207 207 # Returns true if the current user is allowed to edit the page, otherwise false
208 208 def editable?(page = @page)
209 209 page.editable_by?(User.current)
210 210 end
211
212 # Returns the default content of a new wiki page
213 def initial_page_content(page)
214 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
215 extend helper unless self.instance_of?(helper)
216 helper.instance_method(:initial_page_content).bind(self).call(page)
217 end
211 218 end
@@ -1,573 +1,571
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 'coderay'
19 19 require 'coderay/helpers/file_type'
20 require 'forwardable'
20 21
21 22 module ApplicationHelper
22 23 include Redmine::WikiFormatting::Macros::Definitions
23 24
25 extend Forwardable
26 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
27
24 28 def current_role
25 29 @current_role ||= User.current.role_for_project(@project)
26 30 end
27 31
28 32 # Return true if user is authorized for controller/action, otherwise false
29 33 def authorize_for(controller, action)
30 34 User.current.allowed_to?({:controller => controller, :action => action}, @project)
31 35 end
32 36
33 37 # Display a link if user is authorized
34 38 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
35 39 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
36 40 end
37 41
38 42 # Display a link to remote if user is authorized
39 43 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
40 44 url = options[:url] || {}
41 45 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
42 46 end
43 47
44 48 # Display a link to user's account page
45 49 def link_to_user(user)
46 50 user ? link_to(user, :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
47 51 end
48 52
49 53 def link_to_issue(issue, options={})
50 54 options[:class] ||= ''
51 55 options[:class] << ' issue'
52 56 options[:class] << ' closed' if issue.closed?
53 57 link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options
54 58 end
55 59
56 60 # Generates a link to an attachment.
57 61 # Options:
58 62 # * :text - Link text (default to attachment filename)
59 63 # * :download - Force download (default: false)
60 64 def link_to_attachment(attachment, options={})
61 65 text = options.delete(:text) || attachment.filename
62 66 action = options.delete(:download) ? 'download' : 'show'
63 67
64 68 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
65 69 end
66 70
67 71 def toggle_link(name, id, options={})
68 72 onclick = "Element.toggle('#{id}'); "
69 73 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
70 74 onclick << "return false;"
71 75 link_to(name, "#", :onclick => onclick)
72 76 end
73 77
74 78 def image_to_function(name, function, html_options = {})
75 79 html_options.symbolize_keys!
76 80 tag(:input, html_options.merge({
77 81 :type => "image", :src => image_path(name),
78 82 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
79 83 }))
80 84 end
81 85
82 86 def prompt_to_remote(name, text, param, url, html_options = {})
83 87 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
84 88 link_to name, {}, html_options
85 89 end
86 90
87 91 def format_date(date)
88 92 return nil unless date
89 93 # "Setting.date_format.size < 2" is a temporary fix (content of date_format setting changed)
90 94 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
91 95 date.strftime(@date_format)
92 96 end
93 97
94 98 def format_time(time, include_date = true)
95 99 return nil unless time
96 100 time = time.to_time if time.is_a?(String)
97 101 zone = User.current.time_zone
98 102 local = zone ? time.in_time_zone(zone) : (time.utc? ? time.localtime : time)
99 103 @date_format ||= (Setting.date_format.blank? || Setting.date_format.size < 2 ? l(:general_fmt_date) : Setting.date_format)
100 104 @time_format ||= (Setting.time_format.blank? ? l(:general_fmt_time) : Setting.time_format)
101 105 include_date ? local.strftime("#{@date_format} #{@time_format}") : local.strftime(@time_format)
102 106 end
103 107
104 108 def distance_of_date_in_words(from_date, to_date = 0)
105 109 from_date = from_date.to_date if from_date.respond_to?(:to_date)
106 110 to_date = to_date.to_date if to_date.respond_to?(:to_date)
107 111 distance_in_days = (to_date - from_date).abs
108 112 lwr(:actionview_datehelper_time_in_words_day, distance_in_days)
109 113 end
110 114
111 115 def due_date_distance_in_words(date)
112 116 if date
113 117 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
114 118 end
115 119 end
116 120
117 121 # Truncates and returns the string as a single line
118 122 def truncate_single_line(string, *args)
119 123 truncate(string, *args).gsub(%r{[\r\n]+}m, ' ')
120 124 end
121 125
122 126 def html_hours(text)
123 127 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
124 128 end
125 129
126 130 def authoring(created, author)
127 131 time_tag = content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created))
128 132 author_tag = (author.is_a?(User) && !author.anonymous?) ? link_to(h(author), :controller => 'account', :action => 'show', :id => author) : h(author || 'Anonymous')
129 133 l(:label_added_time_by, author_tag, time_tag)
130 134 end
131 135
132 136 def l_or_humanize(s, options={})
133 137 k = "#{options[:prefix]}#{s}".to_sym
134 138 l_has_string?(k) ? l(k) : s.to_s.humanize
135 139 end
136 140
137 141 def day_name(day)
138 142 l(:general_day_names).split(',')[day-1]
139 143 end
140 144
141 145 def month_name(month)
142 146 l(:actionview_datehelper_select_month_names).split(',')[month-1]
143 147 end
144 148
145 149 def syntax_highlight(name, content)
146 150 type = CodeRay::FileType[name]
147 151 type ? CodeRay.scan(content, type).html : h(content)
148 152 end
149 153
150 154 def to_path_param(path)
151 155 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
152 156 end
153 157
154 158 def pagination_links_full(paginator, count=nil, options={})
155 159 page_param = options.delete(:page_param) || :page
156 160 url_param = params.dup
157 161 # don't reuse params if filters are present
158 162 url_param.clear if url_param.has_key?(:set_filter)
159 163
160 164 html = ''
161 165 html << link_to_remote(('&#171; ' + l(:label_previous)),
162 166 {:update => 'content',
163 167 :url => url_param.merge(page_param => paginator.current.previous),
164 168 :complete => 'window.scrollTo(0,0)'},
165 169 {:href => url_for(:params => url_param.merge(page_param => paginator.current.previous))}) + ' ' if paginator.current.previous
166 170
167 171 html << (pagination_links_each(paginator, options) do |n|
168 172 link_to_remote(n.to_s,
169 173 {:url => {:params => url_param.merge(page_param => n)},
170 174 :update => 'content',
171 175 :complete => 'window.scrollTo(0,0)'},
172 176 {:href => url_for(:params => url_param.merge(page_param => n))})
173 177 end || '')
174 178
175 179 html << ' ' + link_to_remote((l(:label_next) + ' &#187;'),
176 180 {:update => 'content',
177 181 :url => url_param.merge(page_param => paginator.current.next),
178 182 :complete => 'window.scrollTo(0,0)'},
179 183 {:href => url_for(:params => url_param.merge(page_param => paginator.current.next))}) if paginator.current.next
180 184
181 185 unless count.nil?
182 186 html << [" (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})", per_page_links(paginator.items_per_page)].compact.join(' | ')
183 187 end
184 188
185 189 html
186 190 end
187 191
188 192 def per_page_links(selected=nil)
189 193 url_param = params.dup
190 194 url_param.clear if url_param.has_key?(:set_filter)
191 195
192 196 links = Setting.per_page_options_array.collect do |n|
193 197 n == selected ? n : link_to_remote(n, {:update => "content", :url => params.dup.merge(:per_page => n)},
194 198 {:href => url_for(url_param.merge(:per_page => n))})
195 199 end
196 200 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
197 201 end
198 202
199 203 def breadcrumb(*args)
200 204 elements = args.flatten
201 205 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
202 206 end
203 207
204 208 def html_title(*args)
205 209 if args.empty?
206 210 title = []
207 211 title << @project.name if @project
208 212 title += @html_title if @html_title
209 213 title << Setting.app_title
210 214 title.compact.join(' - ')
211 215 else
212 216 @html_title ||= []
213 217 @html_title += args
214 218 end
215 219 end
216 220
217 221 def accesskey(s)
218 222 Redmine::AccessKeys.key_for s
219 223 end
220 224
221 225 # Formats text according to system settings.
222 226 # 2 ways to call this method:
223 227 # * with a String: textilizable(text, options)
224 228 # * with an object and one of its attribute: textilizable(issue, :description, options)
225 229 def textilizable(*args)
226 230 options = args.last.is_a?(Hash) ? args.pop : {}
227 231 case args.size
228 232 when 1
229 233 obj = options[:object]
230 234 text = args.shift
231 235 when 2
232 236 obj = args.shift
233 237 text = obj.send(args.shift).to_s
234 238 else
235 239 raise ArgumentError, 'invalid arguments to textilizable'
236 240 end
237 241 return '' if text.blank?
238 242
239 243 only_path = options.delete(:only_path) == false ? false : true
240 244
241 245 # when using an image link, try to use an attachment, if possible
242 246 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
243 247
244 248 if attachments
245 249 attachments = attachments.sort_by(&:created_on).reverse
246 250 text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(gif|jpg|jpeg|png))!/) do |m|
247 251 style = $1
248 252 filename = $6
249 253 rf = Regexp.new(Regexp.escape(filename), Regexp::IGNORECASE)
250 254 # search for the picture in attachments
251 255 if found = attachments.detect { |att| att.filename =~ rf }
252 256 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
253 257 desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
254 258 alt = desc.blank? ? nil : "(#{desc})"
255 259 "!#{style}#{image_url}#{alt}!"
256 260 else
257 261 "!#{style}#{filename}!"
258 262 end
259 263 end
260 264 end
261 265
262 text = (Setting.text_formatting == 'textile') ?
263 Redmine::WikiFormatting.to_html(text) { |macro, args| exec_macro(macro, obj, args) } :
264 simple_format(auto_link(h(text)))
266 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) }
265 267
266 268 # different methods for formatting wiki links
267 269 case options[:wiki_links]
268 270 when :local
269 271 # used for local links to html files
270 272 format_wiki_link = Proc.new {|project, title, anchor| "#{title}.html" }
271 273 when :anchor
272 274 # used for single-file wiki export
273 275 format_wiki_link = Proc.new {|project, title, anchor| "##{title}" }
274 276 else
275 277 format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) }
276 278 end
277 279
278 280 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
279 281
280 282 # Wiki links
281 283 #
282 284 # Examples:
283 285 # [[mypage]]
284 286 # [[mypage|mytext]]
285 287 # wiki links can refer other project wikis, using project name or identifier:
286 288 # [[project:]] -> wiki starting page
287 289 # [[project:|mytext]]
288 290 # [[project:mypage]]
289 291 # [[project:mypage|mytext]]
290 292 text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
291 293 link_project = project
292 294 esc, all, page, title = $1, $2, $3, $5
293 295 if esc.nil?
294 296 if page =~ /^([^\:]+)\:(.*)$/
295 297 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
296 298 page = $2
297 299 title ||= $1 if page.blank?
298 300 end
299 301
300 302 if link_project && link_project.wiki
301 303 # extract anchor
302 304 anchor = nil
303 305 if page =~ /^(.+?)\#(.+)$/
304 306 page, anchor = $1, $2
305 307 end
306 308 # check if page exists
307 309 wiki_page = link_project.wiki.find_page(page)
308 310 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page), anchor),
309 311 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
310 312 else
311 313 # project or wiki doesn't exist
312 314 title || page
313 315 end
314 316 else
315 317 all
316 318 end
317 319 end
318 320
319 321 # Redmine links
320 322 #
321 323 # Examples:
322 324 # Issues:
323 325 # #52 -> Link to issue #52
324 326 # Changesets:
325 327 # r52 -> Link to revision 52
326 328 # commit:a85130f -> Link to scmid starting with a85130f
327 329 # Documents:
328 330 # document#17 -> Link to document with id 17
329 331 # document:Greetings -> Link to the document with title "Greetings"
330 332 # document:"Some document" -> Link to the document with title "Some document"
331 333 # Versions:
332 334 # version#3 -> Link to version with id 3
333 335 # version:1.0.0 -> Link to version named "1.0.0"
334 336 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
335 337 # Attachments:
336 338 # attachment:file.zip -> Link to the attachment of the current object named file.zip
337 339 # Source files:
338 340 # source:some/file -> Link to the file located at /some/file in the project's repository
339 341 # source:some/file@52 -> Link to the file's revision 52
340 342 # source:some/file#L120 -> Link to line 120 of the file
341 343 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
342 344 # export:some/file -> Force the download of the file
343 345 # Forum messages:
344 346 # message#1218 -> Link to message with id 1218
345 347 text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|\s|<|$)}) do |m|
346 348 leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
347 349 link = nil
348 350 if esc.nil?
349 351 if prefix.nil? && sep == 'r'
350 352 if project && (changeset = project.changesets.find_by_revision(oid))
351 353 link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
352 354 :class => 'changeset',
353 355 :title => truncate_single_line(changeset.comments, 100))
354 356 end
355 357 elsif sep == '#'
356 358 oid = oid.to_i
357 359 case prefix
358 360 when nil
359 361 if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
360 362 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
361 363 :class => (issue.closed? ? 'issue closed' : 'issue'),
362 364 :title => "#{truncate(issue.subject, 100)} (#{issue.status.name})")
363 365 link = content_tag('del', link) if issue.closed?
364 366 end
365 367 when 'document'
366 368 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
367 369 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
368 370 :class => 'document'
369 371 end
370 372 when 'version'
371 373 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
372 374 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
373 375 :class => 'version'
374 376 end
375 377 when 'message'
376 378 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
377 379 link = link_to h(truncate(message.subject, 60)), {:only_path => only_path,
378 380 :controller => 'messages',
379 381 :action => 'show',
380 382 :board_id => message.board,
381 383 :id => message.root,
382 384 :anchor => (message.parent ? "message-#{message.id}" : nil)},
383 385 :class => 'message'
384 386 end
385 387 end
386 388 elsif sep == ':'
387 389 # removes the double quotes if any
388 390 name = oid.gsub(%r{^"(.*)"$}, "\\1")
389 391 case prefix
390 392 when 'document'
391 393 if project && document = project.documents.find_by_title(name)
392 394 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
393 395 :class => 'document'
394 396 end
395 397 when 'version'
396 398 if project && version = project.versions.find_by_name(name)
397 399 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
398 400 :class => 'version'
399 401 end
400 402 when 'commit'
401 403 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
402 404 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
403 405 :class => 'changeset',
404 406 :title => truncate_single_line(changeset.comments, 100)
405 407 end
406 408 when 'source', 'export'
407 409 if project && project.repository
408 410 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
409 411 path, rev, anchor = $1, $3, $5
410 412 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
411 413 :path => to_path_param(path),
412 414 :rev => rev,
413 415 :anchor => anchor,
414 416 :format => (prefix == 'export' ? 'raw' : nil)},
415 417 :class => (prefix == 'export' ? 'source download' : 'source')
416 418 end
417 419 when 'attachment'
418 420 if attachments && attachment = attachments.detect {|a| a.filename == name }
419 421 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
420 422 :class => 'attachment'
421 423 end
422 424 end
423 425 end
424 426 end
425 427 leading + (link || "#{prefix}#{sep}#{oid}")
426 428 end
427 429
428 430 text
429 431 end
430 432
431 433 # Same as Rails' simple_format helper without using paragraphs
432 434 def simple_format_without_paragraph(text)
433 435 text.to_s.
434 436 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
435 437 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
436 438 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
437 439 end
438 440
439 441 def error_messages_for(object_name, options = {})
440 442 options = options.symbolize_keys
441 443 object = instance_variable_get("@#{object_name}")
442 444 if object && !object.errors.empty?
443 445 # build full_messages here with controller current language
444 446 full_messages = []
445 447 object.errors.each do |attr, msg|
446 448 next if msg.nil?
447 449 msg = msg.first if msg.is_a? Array
448 450 if attr == "base"
449 451 full_messages << l(msg)
450 452 else
451 453 full_messages << "&#171; " + (l_has_string?("field_" + attr) ? l("field_" + attr) : object.class.human_attribute_name(attr)) + " &#187; " + l(msg) unless attr == "custom_values"
452 454 end
453 455 end
454 456 # retrieve custom values error messages
455 457 if object.errors[:custom_values]
456 458 object.custom_values.each do |v|
457 459 v.errors.each do |attr, msg|
458 460 next if msg.nil?
459 461 msg = msg.first if msg.is_a? Array
460 462 full_messages << "&#171; " + v.custom_field.name + " &#187; " + l(msg)
461 463 end
462 464 end
463 465 end
464 466 content_tag("div",
465 467 content_tag(
466 468 options[:header_tag] || "span", lwr(:gui_validation_error, full_messages.length) + ":"
467 469 ) +
468 470 content_tag("ul", full_messages.collect { |msg| content_tag("li", msg) }),
469 471 "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation"
470 472 )
471 473 else
472 474 ""
473 475 end
474 476 end
475 477
476 478 def lang_options_for_select(blank=true)
477 479 (blank ? [["(auto)", ""]] : []) +
478 480 GLoc.valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
479 481 end
480 482
481 483 def label_tag_for(name, option_tags = nil, options = {})
482 484 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
483 485 content_tag("label", label_text)
484 486 end
485 487
486 488 def labelled_tabular_form_for(name, object, options, &proc)
487 489 options[:html] ||= {}
488 490 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
489 491 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
490 492 end
491 493
492 494 def back_url_hidden_field_tag
493 495 back_url = params[:back_url] || request.env['HTTP_REFERER']
494 496 hidden_field_tag('back_url', back_url) unless back_url.blank?
495 497 end
496 498
497 499 def check_all_links(form_name)
498 500 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
499 501 " | " +
500 502 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
501 503 end
502 504
503 505 def progress_bar(pcts, options={})
504 506 pcts = [pcts, pcts] unless pcts.is_a?(Array)
505 507 pcts[1] = pcts[1] - pcts[0]
506 508 pcts << (100 - pcts[1] - pcts[0])
507 509 width = options[:width] || '100px;'
508 510 legend = options[:legend] || ''
509 511 content_tag('table',
510 512 content_tag('tr',
511 513 (pcts[0] > 0 ? content_tag('td', '', :width => "#{pcts[0].floor}%;", :class => 'closed') : '') +
512 514 (pcts[1] > 0 ? content_tag('td', '', :width => "#{pcts[1].floor}%;", :class => 'done') : '') +
513 515 (pcts[2] > 0 ? content_tag('td', '', :width => "#{pcts[2].floor}%;", :class => 'todo') : '')
514 516 ), :class => 'progress', :style => "width: #{width};") +
515 517 content_tag('p', legend, :class => 'pourcent')
516 518 end
517 519
518 520 def context_menu_link(name, url, options={})
519 521 options[:class] ||= ''
520 522 if options.delete(:selected)
521 523 options[:class] << ' icon-checked disabled'
522 524 options[:disabled] = true
523 525 end
524 526 if options.delete(:disabled)
525 527 options.delete(:method)
526 528 options.delete(:confirm)
527 529 options.delete(:onclick)
528 530 options[:class] << ' disabled'
529 531 url = '#'
530 532 end
531 533 link_to name, url, options
532 534 end
533 535
534 536 def calendar_for(field_id)
535 537 include_calendar_headers_tags
536 538 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
537 539 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
538 540 end
539 541
540 542 def include_calendar_headers_tags
541 543 unless @calendar_headers_tags_included
542 544 @calendar_headers_tags_included = true
543 545 content_for :header_tags do
544 546 javascript_include_tag('calendar/calendar') +
545 547 javascript_include_tag("calendar/lang/calendar-#{current_language}.js") +
546 548 javascript_include_tag('calendar/calendar-setup') +
547 549 stylesheet_link_tag('calendar')
548 550 end
549 551 end
550 552 end
551 553
552 def wikitoolbar_for(field_id)
553 return '' unless Setting.text_formatting == 'textile'
554
555 help_link = l(:setting_text_formatting) + ': ' +
556 link_to(l(:label_help), compute_public_path('wiki_syntax', 'help', 'html'),
557 :onclick => "window.open(\"#{ compute_public_path('wiki_syntax', 'help', 'html') }\", \"\", \"resizable=yes, location=no, width=300, height=640, menubar=no, status=no, scrollbars=yes\"); return false;")
558
559 javascript_include_tag('jstoolbar/jstoolbar') +
560 javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language}") +
561 javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.setHelpLink('#{help_link}'); toolbar.draw();")
562 end
563
564 554 def content_for(name, content = nil, &block)
565 555 @has_content ||= {}
566 556 @has_content[name] = true
567 557 super(name, content, &block)
568 558 end
569 559
570 560 def has_content?(name)
571 561 (@has_content && @has_content[name]) || false
572 562 end
563
564 private
565
566 def wiki_helper
567 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
568 extend helper
569 return self
570 end
573 571 end
@@ -1,67 +1,67
1 1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2 2 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
3 3 <head>
4 4 <title><%=h html_title %></title>
5 5 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
6 6 <meta name="description" content="<%= Redmine::Info.app_name %>" />
7 7 <meta name="keywords" content="issue,bug,tracker" />
8 8 <%= stylesheet_link_tag 'application', :media => 'all' %>
9 9 <%= javascript_include_tag :defaults %>
10 <%= stylesheet_link_tag 'jstoolbar' %>
10 <%= heads_for_wiki_formatter %>
11 11 <!--[if IE]>
12 12 <style type="text/css">
13 13 * html body{ width: expression( document.documentElement.clientWidth < 900 ? '900px' : '100%' ); }
14 14 body {behavior: url(<%= stylesheet_path "csshover.htc" %>);}
15 15 </style>
16 16 <![endif]-->
17 17 <%= call_hook :view_layouts_base_html_head %>
18 18 <!-- page specific tags -->
19 19 <%= yield :header_tags -%>
20 20 </head>
21 21 <body>
22 22 <div id="wrapper">
23 23 <div id="top-menu">
24 24 <div id="account">
25 25 <%= render_menu :account_menu -%>
26 26 </div>
27 27 <%= content_tag('div', "#{l(:label_logged_as)} #{User.current.login}", :id => 'loggedas') if User.current.logged? %>
28 28 <%= render_menu :top_menu -%>
29 29 </div>
30 30
31 31 <div id="header">
32 32 <div id="quick-search">
33 33 <% form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
34 34 <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>:
35 35 <%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %>
36 36 <% end %>
37 37 <%= render :partial => 'layouts/project_selector' if User.current.memberships.any? %>
38 38 </div>
39 39
40 40 <h1><%= h(@project && !@project.new_record? ? @project.name : Setting.app_title) %></h1>
41 41
42 42 <div id="main-menu">
43 43 <%= render_main_menu(@project) %>
44 44 </div>
45 45 </div>
46 46
47 47 <%= tag('div', {:id => 'main', :class => (has_content?(:sidebar) ? '' : 'nosidebar')}, true) %>
48 48 <div id="sidebar">
49 49 <%= yield :sidebar %>
50 50 </div>
51 51
52 52 <div id="content">
53 53 <%= content_tag('div', flash[:error], :class => 'flash error') if flash[:error] %>
54 54 <%= content_tag('div', flash[:notice], :class => 'flash notice') if flash[:notice] %>
55 55 <%= yield %>
56 56 </div>
57 57 </div>
58 58
59 59 <div id="ajax-indicator" style="display:none;"><span><%= l(:label_loading) %></span></div>
60 60
61 61 <div id="footer">
62 62 Powered by <%= link_to Redmine::Info.app_name, Redmine::Info.url %> &copy; 2006-2008 Jean-Philippe Lang
63 63 </div>
64 64 </div>
65 65 <%= call_hook :view_layouts_base_body_bottom %>
66 66 </body>
67 67 </html>
@@ -1,37 +1,37
1 1 <html>
2 2 <head>
3 3 <style>
4 4 body {
5 5 font-family: Verdana, sans-serif;
6 6 font-size: 0.8em;
7 7 color:#484848;
8 8 }
9 9 h1 {
10 10 font-family: "Trebuchet MS", Verdana, sans-serif;
11 11 font-size: 1.2em;
12 12 margin: 0px;
13 13 }
14 14 a, a:link, a:visited {
15 15 color: #2A5685;
16 16 }
17 17 a:hover, a:active {
18 18 color: #c61a1a;
19 19 }
20 20 hr {
21 21 width: 100%;
22 22 height: 1px;
23 23 background: #ccc;
24 24 border: 0;
25 25 }
26 26 .footer {
27 27 font-size: 0.8em;
28 28 font-style: italic;
29 29 }
30 30 </style>
31 31 </head>
32 32 <body>
33 33 <%= yield %>
34 34 <hr />
35 <span class="footer"><%= Redmine::WikiFormatting.to_html(Setting.emails_footer) %></span>
35 <span class="footer"><%= Redmine::WikiFormatting.to_html(Setting.text_formatting, Setting.emails_footer) %></span>
36 36 </body>
37 37 </html>
@@ -1,52 +1,52
1 1 <% form_tag({:action => 'edit'}) do %>
2 2
3 3 <div class="box tabular settings">
4 4 <p><label><%= l(:setting_app_title) %></label>
5 5 <%= text_field_tag 'settings[app_title]', Setting.app_title, :size => 30 %></p>
6 6
7 7 <p><label><%= l(:setting_welcome_text) %></label>
8 8 <%= text_area_tag 'settings[welcome_text]', Setting.welcome_text, :cols => 60, :rows => 5, :class => 'wiki-edit' %></p>
9 9 <%= wikitoolbar_for 'settings[welcome_text]' %>
10 10
11 11 <p><label><%= l(:label_theme) %></label>
12 12 <%= select_tag 'settings[ui_theme]', options_for_select( ([[l(:label_default), '']] + Redmine::Themes.themes.collect {|t| [t.name, t.id]}), Setting.ui_theme) %></p>
13 13
14 14 <p><label><%= l(:setting_default_language) %></label>
15 15 <%= select_tag 'settings[default_language]', options_for_select( lang_options_for_select(false), Setting.default_language) %></p>
16 16
17 17 <p><label><%= l(:setting_date_format) %></label>
18 18 <%= select_tag 'settings[date_format]', options_for_select( [[l(:label_language_based), '']] + Setting::DATE_FORMATS.collect {|f| [Date.today.strftime(f), f]}, Setting.date_format) %></p>
19 19
20 20 <p><label><%= l(:setting_time_format) %></label>
21 21 <%= select_tag 'settings[time_format]', options_for_select( [[l(:label_language_based), '']] + Setting::TIME_FORMATS.collect {|f| [Time.now.strftime(f), f]}, Setting.time_format) %></p>
22 22
23 23 <p><label><%= l(:setting_user_format) %></label>
24 24 <%= select_tag 'settings[user_format]', options_for_select( @options[:user_format], Setting.user_format.to_s ) %></p>
25 25
26 26 <p><label><%= l(:setting_attachment_max_size) %></label>
27 27 <%= text_field_tag 'settings[attachment_max_size]', Setting.attachment_max_size, :size => 6 %> KB</p>
28 28
29 29 <p><label><%= l(:setting_per_page_options) %></label>
30 30 <%= text_field_tag 'settings[per_page_options]', Setting.per_page_options_array.join(', '), :size => 20 %><br /><em><%= l(:text_comma_separated) %></em></p>
31 31
32 32 <p><label><%= l(:setting_activity_days_default) %></label>
33 33 <%= text_field_tag 'settings[activity_days_default]', Setting.activity_days_default, :size => 6 %> <%= l(:label_day_plural) %></p>
34 34
35 35 <p><label><%= l(:setting_host_name) %></label>
36 36 <%= text_field_tag 'settings[host_name]', Setting.host_name, :size => 60 %></p>
37 37
38 38 <p><label><%= l(:setting_protocol) %></label>
39 39 <%= select_tag 'settings[protocol]', options_for_select(['http', 'https'], Setting.protocol) %></p>
40 40
41 41 <p><label><%= l(:setting_text_formatting) %></label>
42 <%= select_tag 'settings[text_formatting]', options_for_select([[l(:label_none), "0"], ["textile", "textile"]], Setting.text_formatting) %></p>
42 <%= select_tag 'settings[text_formatting]', options_for_select([[l(:label_none), "0"], *Redmine::WikiFormatting.format_names.collect{|name| [name, name]} ], Setting.text_formatting.to_sym) %></p>
43 43
44 44 <p><label><%= l(:setting_wiki_compression) %></label>
45 45 <%= select_tag 'settings[wiki_compression]', options_for_select( [[l(:label_none), 0], ["gzip", "gzip"]], Setting.wiki_compression) %></p>
46 46
47 47 <p><label><%= l(:setting_feeds_limit) %></label>
48 48 <%= text_field_tag 'settings[feeds_limit]', Setting.feeds_limit, :size => 6 %></p>
49 49 </div>
50 50
51 51 <%= submit_tag l(:button_save) %>
52 52 <% end %>
@@ -1,152 +1,157
1 1 require 'redmine/access_control'
2 2 require 'redmine/menu_manager'
3 3 require 'redmine/activity'
4 4 require 'redmine/mime_type'
5 5 require 'redmine/core_ext'
6 6 require 'redmine/themes'
7 7 require 'redmine/hook'
8 8 require 'redmine/plugin'
9 require 'redmine/wiki_formatting'
9 10
10 11 begin
11 12 require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick)
12 13 rescue LoadError
13 14 # RMagick is not available
14 15 end
15 16
16 17 REDMINE_SUPPORTED_SCM = %w( Subversion Darcs Mercurial Cvs Bazaar Git Filesystem )
17 18
18 19 # Permissions
19 20 Redmine::AccessControl.map do |map|
20 21 map.permission :view_project, {:projects => [:show, :activity]}, :public => true
21 22 map.permission :search_project, {:search => :index}, :public => true
22 23 map.permission :edit_project, {:projects => [:settings, :edit]}, :require => :member
23 24 map.permission :select_project_modules, {:projects => :modules}, :require => :member
24 25 map.permission :manage_members, {:projects => :settings, :members => [:new, :edit, :destroy]}, :require => :member
25 26 map.permission :manage_versions, {:projects => [:settings, :add_version], :versions => [:edit, :destroy]}, :require => :member
26 27
27 28 map.project_module :issue_tracking do |map|
28 29 # Issue categories
29 30 map.permission :manage_categories, {:projects => [:settings, :add_issue_category], :issue_categories => [:edit, :destroy]}, :require => :member
30 31 # Issues
31 32 map.permission :view_issues, {:projects => [:changelog, :roadmap],
32 33 :issues => [:index, :changes, :show, :context_menu],
33 34 :versions => [:show, :status_by],
34 35 :queries => :index,
35 36 :reports => :issue_report}, :public => true
36 37 map.permission :add_issues, {:issues => :new}
37 38 map.permission :edit_issues, {:issues => [:edit, :reply, :bulk_edit, :destroy_attachment]}
38 39 map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
39 40 map.permission :add_issue_notes, {:issues => [:edit, :reply]}
40 41 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
41 42 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
42 43 map.permission :move_issues, {:issues => :move}, :require => :loggedin
43 44 map.permission :delete_issues, {:issues => :destroy}, :require => :member
44 45 # Queries
45 46 map.permission :manage_public_queries, {:queries => [:new, :edit, :destroy]}, :require => :member
46 47 map.permission :save_queries, {:queries => [:new, :edit, :destroy]}, :require => :loggedin
47 48 # Gantt & calendar
48 49 map.permission :view_gantt, :issues => :gantt
49 50 map.permission :view_calendar, :issues => :calendar
50 51 # Watchers
51 52 map.permission :view_issue_watchers, {}
52 53 map.permission :add_issue_watchers, {:watchers => :new}
53 54 end
54 55
55 56 map.project_module :time_tracking do |map|
56 57 map.permission :log_time, {:timelog => :edit}, :require => :loggedin
57 58 map.permission :view_time_entries, :timelog => [:details, :report]
58 59 map.permission :edit_time_entries, {:timelog => [:edit, :destroy]}, :require => :member
59 60 map.permission :edit_own_time_entries, {:timelog => [:edit, :destroy]}, :require => :loggedin
60 61 end
61 62
62 63 map.project_module :news do |map|
63 64 map.permission :manage_news, {:news => [:new, :edit, :destroy, :destroy_comment]}, :require => :member
64 65 map.permission :view_news, {:news => [:index, :show]}, :public => true
65 66 map.permission :comment_news, {:news => :add_comment}
66 67 end
67 68
68 69 map.project_module :documents do |map|
69 70 map.permission :manage_documents, {:documents => [:new, :edit, :destroy, :add_attachment, :destroy_attachment]}, :require => :loggedin
70 71 map.permission :view_documents, :documents => [:index, :show, :download]
71 72 end
72 73
73 74 map.project_module :files do |map|
74 75 map.permission :manage_files, {:projects => :add_file, :versions => :destroy_file}, :require => :loggedin
75 76 map.permission :view_files, :projects => :list_files, :versions => :download
76 77 end
77 78
78 79 map.project_module :wiki do |map|
79 80 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
80 81 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
81 82 map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
82 83 map.permission :view_wiki_pages, :wiki => [:index, :special]
83 84 map.permission :view_wiki_edits, :wiki => [:history, :diff, :annotate]
84 85 map.permission :edit_wiki_pages, :wiki => [:edit, :preview, :add_attachment]
85 86 map.permission :delete_wiki_pages_attachments, :wiki => :destroy_attachment
86 87 map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
87 88 end
88 89
89 90 map.project_module :repository do |map|
90 91 map.permission :manage_repository, {:repositories => [:edit, :destroy]}, :require => :member
91 92 map.permission :browse_repository, :repositories => [:show, :browse, :entry, :annotate, :changes, :diff, :stats, :graph]
92 93 map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
93 94 map.permission :commit_access, {}
94 95 end
95 96
96 97 map.project_module :boards do |map|
97 98 map.permission :manage_boards, {:boards => [:new, :edit, :destroy]}, :require => :member
98 99 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
99 100 map.permission :add_messages, {:messages => [:new, :reply, :quote]}
100 101 map.permission :edit_messages, {:messages => :edit}, :require => :member
101 102 map.permission :delete_messages, {:messages => :destroy}, :require => :member
102 103 end
103 104 end
104 105
105 106 Redmine::MenuManager.map :top_menu do |menu|
106 107 menu.push :home, :home_path, :html => { :class => 'home' }
107 108 menu.push :my_page, { :controller => 'my', :action => 'page' }, :html => { :class => 'mypage' }, :if => Proc.new { User.current.logged? }
108 109 menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural, :html => { :class => 'projects' }
109 110 menu.push :administration, { :controller => 'admin', :action => 'index' }, :html => { :class => 'admin' }, :if => Proc.new { User.current.admin? }, :last => true
110 111 menu.push :help, Redmine::Info.help_url, :html => { :class => 'help' }, :last => true
111 112 end
112 113
113 114 Redmine::MenuManager.map :account_menu do |menu|
114 115 menu.push :login, :signin_path, :html => { :class => 'login' }, :if => Proc.new { !User.current.logged? }
115 116 menu.push :register, { :controller => 'account', :action => 'register' }, :html => { :class => 'register' }, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
116 117 menu.push :my_account, { :controller => 'my', :action => 'account' }, :html => { :class => 'myaccount' }, :if => Proc.new { User.current.logged? }
117 118 menu.push :logout, :signout_path, :html => { :class => 'logout' }, :if => Proc.new { User.current.logged? }
118 119 end
119 120
120 121 Redmine::MenuManager.map :application_menu do |menu|
121 122 # Empty
122 123 end
123 124
124 125 Redmine::MenuManager.map :project_menu do |menu|
125 126 menu.push :overview, { :controller => 'projects', :action => 'show' }
126 127 menu.push :activity, { :controller => 'projects', :action => 'activity' }
127 128 menu.push :roadmap, { :controller => 'projects', :action => 'roadmap' },
128 129 :if => Proc.new { |p| p.versions.any? }
129 130 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
130 131 menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new,
131 132 :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
132 133 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
133 134 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
134 135 menu.push :wiki, { :controller => 'wiki', :action => 'index', :page => nil },
135 136 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
136 137 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
137 138 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
138 139 menu.push :files, { :controller => 'projects', :action => 'list_files' }, :caption => :label_attachment_plural
139 140 menu.push :repository, { :controller => 'repositories', :action => 'show' },
140 141 :if => Proc.new { |p| p.repository && !p.repository.new_record? }
141 142 menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
142 143 end
143 144
144 145 Redmine::Activity.map do |activity|
145 146 activity.register :issues, :class_name => %w(Issue Journal)
146 147 activity.register :changesets
147 148 activity.register :news
148 149 activity.register :documents, :class_name => %w(Document Attachment)
149 150 activity.register :files, :class_name => 'Attachment'
150 151 activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false
151 152 activity.register :messages, :default => false
152 153 end
154
155 Redmine::WikiFormatting.map do |format|
156 format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper
157 end
@@ -1,157 +1,167
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 module Redmine #:nodoc:
19 19
20 20 # Base class for Redmine plugins.
21 21 # Plugins are registered using the <tt>register</tt> class method that acts as the public constructor.
22 22 #
23 23 # Redmine::Plugin.register :example do
24 24 # name 'Example plugin'
25 25 # author 'John Smith'
26 26 # description 'This is an example plugin for Redmine'
27 27 # version '0.0.1'
28 28 # settings :default => {'foo'=>'bar'}, :partial => 'settings/settings'
29 29 # end
30 30 #
31 31 # === Plugin attributes
32 32 #
33 33 # +settings+ is an optional attribute that let the plugin be configurable.
34 34 # It must be a hash with the following keys:
35 35 # * <tt>:default</tt>: default value for the plugin settings
36 36 # * <tt>:partial</tt>: path of the configuration partial view, relative to the plugin <tt>app/views</tt> directory
37 37 # Example:
38 38 # settings :default => {'foo'=>'bar'}, :partial => 'settings/settings'
39 39 # In this example, the settings partial will be found here in the plugin directory: <tt>app/views/settings/_settings.rhtml</tt>.
40 40 #
41 41 # When rendered, the plugin settings value is available as the local variable +settings+
42 42 class Plugin
43 43 @registered_plugins = {}
44 44 class << self
45 45 attr_reader :registered_plugins
46 46 private :new
47 47
48 48 def def_field(*names)
49 49 class_eval do
50 50 names.each do |name|
51 51 define_method(name) do |*args|
52 52 args.empty? ? instance_variable_get("@#{name}") : instance_variable_set("@#{name}", *args)
53 53 end
54 54 end
55 55 end
56 56 end
57 57 end
58 58 def_field :name, :description, :author, :version, :settings
59 59
60 60 # Plugin constructor
61 61 def self.register(name, &block)
62 62 p = new
63 63 p.instance_eval(&block)
64 64 Plugin.registered_plugins[name] = p
65 65 end
66 66
67 67 # Adds an item to the given +menu+.
68 68 # The +id+ parameter (equals to the project id) is automatically added to the url.
69 69 # menu :project_menu, :plugin_example, { :controller => 'example', :action => 'say_hello' }, :caption => 'Sample'
70 70 #
71 71 # +name+ parameter can be: :top_menu, :account_menu, :application_menu or :project_menu
72 72 #
73 73 def menu(menu, item, url, options={})
74 74 Redmine::MenuManager.map(menu).push(item, url, options)
75 75 end
76 76 alias :add_menu_item :menu
77 77
78 78 # Removes +item+ from the given +menu+.
79 79 def delete_menu_item(menu, item)
80 80 Redmine::MenuManager.map(menu).delete(item)
81 81 end
82 82
83 83 # Defines a permission called +name+ for the given +actions+.
84 84 #
85 85 # The +actions+ argument is a hash with controllers as keys and actions as values (a single value or an array):
86 86 # permission :destroy_contacts, { :contacts => :destroy }
87 87 # permission :view_contacts, { :contacts => [:index, :show] }
88 88 #
89 89 # The +options+ argument can be used to make the permission public (implicitly given to any user)
90 90 # or to restrict users the permission can be given to.
91 91 #
92 92 # Examples
93 93 # # A permission that is implicitly given to any user
94 94 # # This permission won't appear on the Roles & Permissions setup screen
95 95 # permission :say_hello, { :example => :say_hello }, :public => true
96 96 #
97 97 # # A permission that can be given to any user
98 98 # permission :say_hello, { :example => :say_hello }
99 99 #
100 100 # # A permission that can be given to registered users only
101 101 # permission :say_hello, { :example => :say_hello }, :require => loggedin
102 102 #
103 103 # # A permission that can be given to project members only
104 104 # permission :say_hello, { :example => :say_hello }, :require => member
105 105 def permission(name, actions, options = {})
106 106 if @project_module
107 107 Redmine::AccessControl.map {|map| map.project_module(@project_module) {|map|map.permission(name, actions, options)}}
108 108 else
109 109 Redmine::AccessControl.map {|map| map.permission(name, actions, options)}
110 110 end
111 111 end
112 112
113 113 # Defines a project module, that can be enabled/disabled for each project.
114 114 # Permissions defined inside +block+ will be bind to the module.
115 115 #
116 116 # project_module :things do
117 117 # permission :view_contacts, { :contacts => [:list, :show] }, :public => true
118 118 # permission :destroy_contacts, { :contacts => :destroy }
119 119 # end
120 120 def project_module(name, &block)
121 121 @project_module = name
122 122 self.instance_eval(&block)
123 123 @project_module = nil
124 124 end
125 125
126 126 # Registers an activity provider.
127 127 #
128 128 # Options:
129 129 # * <tt>:class_name</tt> - one or more model(s) that provide these events (inferred from event_type by default)
130 130 # * <tt>:default</tt> - setting this option to false will make the events not displayed by default
131 131 #
132 132 # A model can provide several activity event types.
133 133 #
134 134 # Examples:
135 135 # register :news
136 136 # register :scrums, :class_name => 'Meeting'
137 137 # register :issues, :class_name => ['Issue', 'Journal']
138 138 #
139 139 # Retrieving events:
140 140 # Associated model(s) must implement the find_events class method.
141 141 # ActiveRecord models can use acts_as_activity_provider as a way to implement this class method.
142 142 #
143 143 # The following call should return all the scrum events visible by current user that occured in the 5 last days:
144 144 # Meeting.find_events('scrums', User.current, 5.days.ago, Date.today)
145 145 # Meeting.find_events('scrums', User.current, 5.days.ago, Date.today, :project => foo) # events for project foo only
146 146 #
147 147 # Note that :view_scrums permission is required to view these events in the activity view.
148 148 def activity_provider(*args)
149 149 Redmine::Activity.register(*args)
150 150 end
151
152 # Registers a wiki formatter.
153 #
154 # Parameters:
155 # * +name+ - human-readable name
156 # * +formatter+ - formatter class, which should have an instance method +to_html+
157 # * +helper+ - helper module, which will be included by wiki pages
158 def wiki_format_provider(name, formatter, helper)
159 Redmine::WikiFormatting.register(name, formatter, helper)
160 end
151 161
152 162 # Returns +true+ if the plugin can be configured.
153 163 def configurable?
154 164 settings && settings.is_a?(Hash) && !settings[:partial].blank?
155 165 end
156 166 end
157 167 end
@@ -1,190 +1,79
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 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 require 'redcloth3'
19 require 'coderay'
20
21 18 module Redmine
22 19 module WikiFormatting
23
24 private
25
26 class TextileFormatter < RedCloth3
27
28 # auto_link rule after textile rules so that it doesn't break !image_url! tags
29 RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto, :inline_toc, :inline_macros]
20 @@formatters = {}
21
22 class << self
23 def map
24 yield self
25 end
30 26
31 def initialize(*args)
32 super
33 self.hard_breaks=true
34 self.no_span_caps=true
27 def register(name, formatter, helper)
28 raise ArgumentError, "format name '#{name}' is already taken" if @@formatters[name]
29 @@formatters[name.to_sym] = {:formatter => formatter, :helper => helper}
35 30 end
36 31
37 def to_html(*rules, &block)
38 @toc = []
39 @macros_runner = block
40 super(*RULES).to_s
32 def formatter_for(name)
33 entry = @@formatters[name.to_sym]
34 (entry && entry[:formatter]) || Redmine::WikiFormatting::NullFormatter::Formatter
41 35 end
42
43 private
44
45 # Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet.
46 # <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a>
47 def hard_break( text )
48 text.gsub!( /(.)\n(?!\n|\Z|>| *(>? *[#*=]+(\s|$)|[{|]))/, "\\1<br />\n" ) if hard_breaks
36
37 def helper_for(name)
38 entry = @@formatters[name.to_sym]
39 (entry && entry[:helper]) || Redmine::WikiFormatting::NullFormatter::Helper
49 40 end
50 41
51 # Patch to add code highlighting support to RedCloth
52 def smooth_offtags( text )
53 unless @pre_list.empty?
54 ## replace <pre> content
55 text.gsub!(/<redpre#(\d+)>/) do
56 content = @pre_list[$1.to_i]
57 if content.match(/<code\s+class="(\w+)">\s?(.+)/m)
58 content = "<code class=\"#{$1} CodeRay\">" +
59 CodeRay.scan($2, $1.downcase).html(:escape => false, :line_numbers => :inline)
60 end
61 content
62 end
63 end
42 def format_names
43 @@formatters.keys.map
64 44 end
65 45
66 # Patch to add 'table of content' support to RedCloth
67 def textile_p_withtoc(tag, atts, cite, content)
68 # removes wiki links from the item
69 toc_item = content.gsub(/(\[\[|\]\])/, '')
70 # removes styles
71 # eg. %{color:red}Triggers% => Triggers
72 toc_item.gsub! %r[%\{[^\}]*\}([^%]+)%], '\\1'
46 def to_html(format, text, options = {}, &block)
47 formatter_for(format).new(text).to_html(&block)
48 end
49 end
50
51 # Default formatter module
52 module NullFormatter
53 class Formatter
54 include ActionView::Helpers::TagHelper
55 include ActionView::Helpers::TextHelper
73 56
74 # replaces non word caracters by dashes
75 anchor = toc_item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
76
77 unless anchor.blank?
78 if tag =~ /^h(\d)$/
79 @toc << [$1.to_i, anchor, toc_item]
80 end
81 atts << " id=\"#{anchor}\""
82 content = content + "<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a>"
57 def initialize(text)
58 @text = text
83 59 end
84 textile_p(tag, atts, cite, content)
85 end
86
87 alias :textile_h1 :textile_p_withtoc
88 alias :textile_h2 :textile_p_withtoc
89 alias :textile_h3 :textile_p_withtoc
90
91 def inline_toc(text)
92 text.gsub!(/<p>\{\{([<>]?)toc\}\}<\/p>/i) do
93 div_class = 'toc'
94 div_class << ' right' if $1 == '>'
95 div_class << ' left' if $1 == '<'
96 out = "<ul class=\"#{div_class}\">"
97 @toc.each do |heading|
98 level, anchor, toc_item = heading
99 out << "<li class=\"heading#{level}\"><a href=\"##{anchor}\">#{toc_item}</a></li>\n"
100 end
101 out << '</ul>'
102 out
60
61 def to_html(*args)
62 simple_format(auto_link(CGI::escapeHTML(@text)))
103 63 end
104 64 end
105 65
106 MACROS_RE = /
107 (!)? # escaping
108 (
109 \{\{ # opening tag
110 ([\w]+) # macro name
111 (\(([^\}]*)\))? # optional arguments
112 \}\} # closing tag
113 )
114 /x unless const_defined?(:MACROS_RE)
115
116 def inline_macros(text)
117 text.gsub!(MACROS_RE) do
118 esc, all, macro = $1, $2, $3.downcase
119 args = ($5 || '').split(',').each(&:strip)
120 if esc.nil?
121 begin
122 @macros_runner.call(macro, args)
123 rescue => e
124 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
125 end || all
126 else
127 all
128 end
66 module Helper
67 def wikitoolbar_for(field_id)
129 68 end
130 end
131 69
132 AUTO_LINK_RE = %r{
133 ( # leading text
134 <\w+.*?>| # leading HTML tag, or
135 [^=<>!:'"/]| # leading punctuation, or
136 ^ # beginning of line
137 )
138 (
139 (?:https?://)| # protocol spec, or
140 (?:ftp://)|
141 (?:www\.) # www.*
142 )
143 (
144 (\S+?) # url
145 (\/)? # slash
146 )
147 ([^\w\=\/;\(\)]*?) # post
148 (?=<|\s|$)
149 }x unless const_defined?(:AUTO_LINK_RE)
150
151 # Turns all urls into clickable links (code from Rails).
152 def inline_auto_link(text)
153 text.gsub!(AUTO_LINK_RE) do
154 all, leading, proto, url, post = $&, $1, $2, $3, $6
155 if leading =~ /<a\s/i || leading =~ /![<>=]?/
156 # don't replace URL's that are already linked
157 # and URL's prefixed with ! !> !< != (textile images)
158 all
159 else
160 # Idea below : an URL with unbalanced parethesis and
161 # ending by ')' is put into external parenthesis
162 if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
163 url=url[0..-2] # discard closing parenth from url
164 post = ")"+post # add closing parenth to post
165 end
166 %(#{leading}<a class="external" href="#{proto=="www."?"http://www.":proto}#{url}">#{proto + url}</a>#{post})
167 end
70 def heads_for_wiki_formatter
168 71 end
169 end
170
171 # Turns all email addresses into clickable links (code from Rails).
172 def inline_auto_mailto(text)
173 text.gsub!(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
174 mail = $1
175 if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/)
176 mail
177 else
178 %{<a href="mailto:#{mail}" class="email">#{mail}</a>}
179 end
72
73 def initial_page_content(page)
74 page.pretty_title.to_s
180 75 end
181 76 end
182 77 end
183
184 public
185
186 def self.to_html(text, options = {}, &block)
187 TextileFormatter.new(text).to_html(&block)
188 end
189 78 end
190 79 end
@@ -1,559 +1,380
1 1 /* ***** BEGIN LICENSE BLOCK *****
2 2 * This file is part of DotClear.
3 3 * Copyright (c) 2005 Nicolas Martin & Olivier Meunier and contributors. All
4 4 * rights reserved.
5 5 *
6 6 * DotClear is free software; you can redistribute it and/or modify
7 7 * it under the terms of the GNU General Public License as published by
8 8 * the Free Software Foundation; either version 2 of the License, or
9 9 * (at your option) any later version.
10 10 *
11 11 * DotClear is distributed in the hope that it will be useful,
12 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 * GNU General Public License for more details.
15 15 *
16 16 * You should have received a copy of the GNU General Public License
17 17 * along with DotClear; if not, write to the Free Software
18 18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 19 *
20 20 * ***** END LICENSE BLOCK *****
21 21 */
22 22
23 23 /* Modified by JP LANG for textile formatting */
24 24
25 25 function jsToolBar(textarea) {
26 26 if (!document.createElement) { return; }
27 27
28 28 if (!textarea) { return; }
29 29
30 30 if ((typeof(document["selection"]) == "undefined")
31 31 && (typeof(textarea["setSelectionRange"]) == "undefined")) {
32 32 return;
33 33 }
34 34
35 35 this.textarea = textarea;
36 36
37 37 this.editor = document.createElement('div');
38 38 this.editor.className = 'jstEditor';
39 39
40 40 this.textarea.parentNode.insertBefore(this.editor,this.textarea);
41 41 this.editor.appendChild(this.textarea);
42 42
43 43 this.toolbar = document.createElement("div");
44 44 this.toolbar.className = 'jstElements';
45 45 this.editor.parentNode.insertBefore(this.toolbar,this.editor);
46 46
47 47 // Dragable resizing (only for gecko)
48 48 if (this.editor.addEventListener)
49 49 {
50 50 this.handle = document.createElement('div');
51 51 this.handle.className = 'jstHandle';
52 52 var dragStart = this.resizeDragStart;
53 53 var This = this;
54 54 this.handle.addEventListener('mousedown',function(event) { dragStart.call(This,event); },false);
55 55 // fix memory leak in Firefox (bug #241518)
56 56 window.addEventListener('unload',function() {
57 57 var del = This.handle.parentNode.removeChild(This.handle);
58 58 delete(This.handle);
59 59 },false);
60 60
61 61 this.editor.parentNode.insertBefore(this.handle,this.editor.nextSibling);
62 62 }
63 63
64 64 this.context = null;
65 65 this.toolNodes = {}; // lorsque la toolbar est dessinΓ©e , cet objet est garni
66 66 // de raccourcis vers les Γ©lΓ©ments DOM correspondants aux outils.
67 67 }
68 68
69 69 function jsButton(title, fn, scope, className) {
70 70 if(typeof jsToolBar.strings == 'undefined') {
71 71 this.title = title || null;
72 72 } else {
73 73 this.title = jsToolBar.strings[title] || title || null;
74 74 }
75 75 this.fn = fn || function(){};
76 76 this.scope = scope || null;
77 77 this.className = className || null;
78 78 }
79 79 jsButton.prototype.draw = function() {
80 80 if (!this.scope) return null;
81 81
82 82 var button = document.createElement('button');
83 83 button.setAttribute('type','button');
84 84 button.tabIndex = 200;
85 85 if (this.className) button.className = this.className;
86 86 button.title = this.title;
87 87 var span = document.createElement('span');
88 88 span.appendChild(document.createTextNode(this.title));
89 89 button.appendChild(span);
90 90
91 91 if (this.icon != undefined) {
92 92 button.style.backgroundImage = 'url('+this.icon+')';
93 93 }
94 94 if (typeof(this.fn) == 'function') {
95 95 var This = this;
96 96 button.onclick = function() { try { This.fn.apply(This.scope, arguments) } catch (e) {} return false; };
97 97 }
98 98 return button;
99 99 }
100 100
101 101 function jsSpace(id) {
102 102 this.id = id || null;
103 103 this.width = null;
104 104 }
105 105 jsSpace.prototype.draw = function() {
106 106 var span = document.createElement('span');
107 107 if (this.id) span.id = this.id;
108 108 span.appendChild(document.createTextNode(String.fromCharCode(160)));
109 109 span.className = 'jstSpacer';
110 110 if (this.width) span.style.marginRight = this.width+'px';
111 111
112 112 return span;
113 113 }
114 114
115 115 function jsCombo(title, options, scope, fn, className) {
116 116 this.title = title || null;
117 117 this.options = options || null;
118 118 this.scope = scope || null;
119 119 this.fn = fn || function(){};
120 120 this.className = className || null;
121 121 }
122 122 jsCombo.prototype.draw = function() {
123 123 if (!this.scope || !this.options) return null;
124 124
125 125 var select = document.createElement('select');
126 126 if (this.className) select.className = className;
127 127 select.title = this.title;
128 128
129 129 for (var o in this.options) {
130 130 //var opt = this.options[o];
131 131 var option = document.createElement('option');
132 132 option.value = o;
133 133 option.appendChild(document.createTextNode(this.options[o]));
134 134 select.appendChild(option);
135 135 }
136 136
137 137 var This = this;
138 138 select.onchange = function() {
139 139 try {
140 140 This.fn.call(This.scope, this.value);
141 141 } catch (e) { alert(e); }
142 142
143 143 return false;
144 144 }
145 145
146 146 return select;
147 147 }
148 148
149 149
150 150 jsToolBar.prototype = {
151 151 base_url: '',
152 152 mode: 'wiki',
153 153 elements: {},
154 154 help_link: '',
155 155
156 156 getMode: function() {
157 157 return this.mode;
158 158 },
159 159
160 160 setMode: function(mode) {
161 161 this.mode = mode || 'wiki';
162 162 },
163 163
164 164 switchMode: function(mode) {
165 165 mode = mode || 'wiki';
166 166 this.draw(mode);
167 167 },
168 168
169 169 setHelpLink: function(link) {
170 170 this.help_link = link;
171 171 },
172 172
173 173 button: function(toolName) {
174 174 var tool = this.elements[toolName];
175 175 if (typeof tool.fn[this.mode] != 'function') return null;
176 176 var b = new jsButton(tool.title, tool.fn[this.mode], this, 'jstb_'+toolName);
177 177 if (tool.icon != undefined) b.icon = tool.icon;
178 178 return b;
179 179 },
180 180 space: function(toolName) {
181 181 var tool = new jsSpace(toolName)
182 182 if (this.elements[toolName].width !== undefined)
183 183 tool.width = this.elements[toolName].width;
184 184 return tool;
185 185 },
186 186 combo: function(toolName) {
187 187 var tool = this.elements[toolName];
188 188 var length = tool[this.mode].list.length;
189 189
190 190 if (typeof tool[this.mode].fn != 'function' || length == 0) {
191 191 return null;
192 192 } else {
193 193 var options = {};
194 194 for (var i=0; i < length; i++) {
195 195 var opt = tool[this.mode].list[i];
196 196 options[opt] = tool.options[opt];
197 197 }
198 198 return new jsCombo(tool.title, options, this, tool[this.mode].fn);
199 199 }
200 200 },
201 201 draw: function(mode) {
202 202 this.setMode(mode);
203 203
204 204 // Empty toolbar
205 205 while (this.toolbar.hasChildNodes()) {
206 206 this.toolbar.removeChild(this.toolbar.firstChild)
207 207 }
208 208 this.toolNodes = {}; // vide les raccourcis DOM/**/
209 209
210 210 var h = document.createElement('div');
211 211 h.className = 'help'
212 212 h.innerHTML = this.help_link;
213 213 '<a href="/help/wiki_syntax.html" onclick="window.open(\'/help/wiki_syntax.html\', \'\', \'resizable=yes, location=no, width=300, height=640, menubar=no, status=no, scrollbars=yes\'); return false;">Aide</a>';
214 214 this.toolbar.appendChild(h);
215 215
216 216 // Draw toolbar elements
217 217 var b, tool, newTool;
218 218
219 219 for (var i in this.elements) {
220 220 b = this.elements[i];
221 221
222 222 var disabled =
223 223 b.type == undefined || b.type == ''
224 224 || (b.disabled != undefined && b.disabled)
225 225 || (b.context != undefined && b.context != null && b.context != this.context);
226 226
227 227 if (!disabled && typeof this[b.type] == 'function') {
228 228 tool = this[b.type](i);
229 229 if (tool) newTool = tool.draw();
230 230 if (newTool) {
231 231 this.toolNodes[i] = newTool; //mémorise l'accès DOM pour usage éventuel ultérieur
232 232 this.toolbar.appendChild(newTool);
233 233 }
234 234 }
235 235 }
236 236 },
237 237
238 238 singleTag: function(stag,etag) {
239 239 stag = stag || null;
240 240 etag = etag || stag;
241 241
242 242 if (!stag || !etag) { return; }
243 243
244 244 this.encloseSelection(stag,etag);
245 245 },
246 246
247 247 encloseLineSelection: function(prefix, suffix, fn) {
248 248 this.textarea.focus();
249 249
250 250 prefix = prefix || '';
251 251 suffix = suffix || '';
252 252
253 253 var start, end, sel, scrollPos, subst, res;
254 254
255 255 if (typeof(document["selection"]) != "undefined") {
256 256 sel = document.selection.createRange().text;
257 257 } else if (typeof(this.textarea["setSelectionRange"]) != "undefined") {
258 258 start = this.textarea.selectionStart;
259 259 end = this.textarea.selectionEnd;
260 260 scrollPos = this.textarea.scrollTop;
261 261 // go to the start of the line
262 262 start = this.textarea.value.substring(0, start).replace(/[^\r\n]*$/g,'').length;
263 263 // go to the end of the line
264 264 end = this.textarea.value.length - this.textarea.value.substring(end, this.textarea.value.length).replace(/^[^\r\n]*/, '').length;
265 265 sel = this.textarea.value.substring(start, end);
266 266 }
267 267
268 268 if (sel.match(/ $/)) { // exclude ending space char, if any
269 269 sel = sel.substring(0, sel.length - 1);
270 270 suffix = suffix + " ";
271 271 }
272 272
273 273 if (typeof(fn) == 'function') {
274 274 res = (sel) ? fn.call(this,sel) : fn('');
275 275 } else {
276 276 res = (sel) ? sel : '';
277 277 }
278 278
279 279 subst = prefix + res + suffix;
280 280
281 281 if (typeof(document["selection"]) != "undefined") {
282 282 document.selection.createRange().text = subst;
283 283 var range = this.textarea.createTextRange();
284 284 range.collapse(false);
285 285 range.move('character', -suffix.length);
286 286 range.select();
287 287 } else if (typeof(this.textarea["setSelectionRange"]) != "undefined") {
288 288 this.textarea.value = this.textarea.value.substring(0, start) + subst +
289 289 this.textarea.value.substring(end);
290 290 if (sel) {
291 291 this.textarea.setSelectionRange(start + subst.length, start + subst.length);
292 292 } else {
293 293 this.textarea.setSelectionRange(start + prefix.length, start + prefix.length);
294 294 }
295 295 this.textarea.scrollTop = scrollPos;
296 296 }
297 297 },
298 298
299 299 encloseSelection: function(prefix, suffix, fn) {
300 300 this.textarea.focus();
301 301
302 302 prefix = prefix || '';
303 303 suffix = suffix || '';
304 304
305 305 var start, end, sel, scrollPos, subst, res;
306 306
307 307 if (typeof(document["selection"]) != "undefined") {
308 308 sel = document.selection.createRange().text;
309 309 } else if (typeof(this.textarea["setSelectionRange"]) != "undefined") {
310 310 start = this.textarea.selectionStart;
311 311 end = this.textarea.selectionEnd;
312 312 scrollPos = this.textarea.scrollTop;
313 313 sel = this.textarea.value.substring(start, end);
314 314 }
315 315
316 316 if (sel.match(/ $/)) { // exclude ending space char, if any
317 317 sel = sel.substring(0, sel.length - 1);
318 318 suffix = suffix + " ";
319 319 }
320 320
321 321 if (typeof(fn) == 'function') {
322 322 res = (sel) ? fn.call(this,sel) : fn('');
323 323 } else {
324 324 res = (sel) ? sel : '';
325 325 }
326 326
327 327 subst = prefix + res + suffix;
328 328
329 329 if (typeof(document["selection"]) != "undefined") {
330 330 document.selection.createRange().text = subst;
331 331 var range = this.textarea.createTextRange();
332 332 range.collapse(false);
333 333 range.move('character', -suffix.length);
334 334 range.select();
335 335 // this.textarea.caretPos -= suffix.length;
336 336 } else if (typeof(this.textarea["setSelectionRange"]) != "undefined") {
337 337 this.textarea.value = this.textarea.value.substring(0, start) + subst +
338 338 this.textarea.value.substring(end);
339 339 if (sel) {
340 340 this.textarea.setSelectionRange(start + subst.length, start + subst.length);
341 341 } else {
342 342 this.textarea.setSelectionRange(start + prefix.length, start + prefix.length);
343 343 }
344 344 this.textarea.scrollTop = scrollPos;
345 345 }
346 346 },
347 347
348 348 stripBaseURL: function(url) {
349 349 if (this.base_url != '') {
350 350 var pos = url.indexOf(this.base_url);
351 351 if (pos == 0) {
352 352 url = url.substr(this.base_url.length);
353 353 }
354 354 }
355 355
356 356 return url;
357 357 }
358 358 };
359 359
360 360 /** Resizer
361 361 -------------------------------------------------------- */
362 362 jsToolBar.prototype.resizeSetStartH = function() {
363 363 this.dragStartH = this.textarea.offsetHeight + 0;
364 364 };
365 365 jsToolBar.prototype.resizeDragStart = function(event) {
366 366 var This = this;
367 367 this.dragStartY = event.clientY;
368 368 this.resizeSetStartH();
369 369 document.addEventListener('mousemove', this.dragMoveHdlr=function(event){This.resizeDragMove(event);}, false);
370 370 document.addEventListener('mouseup', this.dragStopHdlr=function(event){This.resizeDragStop(event);}, false);
371 371 };
372 372
373 373 jsToolBar.prototype.resizeDragMove = function(event) {
374 374 this.textarea.style.height = (this.dragStartH+event.clientY-this.dragStartY)+'px';
375 375 };
376 376
377 377 jsToolBar.prototype.resizeDragStop = function(event) {
378 378 document.removeEventListener('mousemove', this.dragMoveHdlr, false);
379 379 document.removeEventListener('mouseup', this.dragStopHdlr, false);
380 380 };
381
382 // Elements definition ------------------------------------
383
384 // strong
385 jsToolBar.prototype.elements.strong = {
386 type: 'button',
387 title: 'Strong',
388 fn: {
389 wiki: function() { this.singleTag('*') }
390 }
391 }
392
393 // em
394 jsToolBar.prototype.elements.em = {
395 type: 'button',
396 title: 'Italic',
397 fn: {
398 wiki: function() { this.singleTag("_") }
399 }
400 }
401
402 // ins
403 jsToolBar.prototype.elements.ins = {
404 type: 'button',
405 title: 'Underline',
406 fn: {
407 wiki: function() { this.singleTag('+') }
408 }
409 }
410
411 // del
412 jsToolBar.prototype.elements.del = {
413 type: 'button',
414 title: 'Deleted',
415 fn: {
416 wiki: function() { this.singleTag('-') }
417 }
418 }
419
420 // code
421 jsToolBar.prototype.elements.code = {
422 type: 'button',
423 title: 'Code',
424 fn: {
425 wiki: function() { this.singleTag('@') }
426 }
427 }
428
429 // spacer
430 jsToolBar.prototype.elements.space1 = {type: 'space'}
431
432 // headings
433 jsToolBar.prototype.elements.h1 = {
434 type: 'button',
435 title: 'Heading 1',
436 fn: {
437 wiki: function() {
438 this.encloseLineSelection('h1. ', '',function(str) {
439 str = str.replace(/^h\d+\.\s+/, '')
440 return str;
441 });
442 }
443 }
444 }
445 jsToolBar.prototype.elements.h2 = {
446 type: 'button',
447 title: 'Heading 2',
448 fn: {
449 wiki: function() {
450 this.encloseLineSelection('h2. ', '',function(str) {
451 str = str.replace(/^h\d+\.\s+/, '')
452 return str;
453 });
454 }
455 }
456 }
457 jsToolBar.prototype.elements.h3 = {
458 type: 'button',
459 title: 'Heading 3',
460 fn: {
461 wiki: function() {
462 this.encloseLineSelection('h3. ', '',function(str) {
463 str = str.replace(/^h\d+\.\s+/, '')
464 return str;
465 });
466 }
467 }
468 }
469
470 // spacer
471 jsToolBar.prototype.elements.space2 = {type: 'space'}
472
473 // ul
474 jsToolBar.prototype.elements.ul = {
475 type: 'button',
476 title: 'Unordered list',
477 fn: {
478 wiki: function() {
479 this.encloseLineSelection('','',function(str) {
480 str = str.replace(/\r/g,'');
481 return str.replace(/(\n|^)[#-]?\s*/g,"$1* ");
482 });
483 }
484 }
485 }
486
487 // ol
488 jsToolBar.prototype.elements.ol = {
489 type: 'button',
490 title: 'Ordered list',
491 fn: {
492 wiki: function() {
493 this.encloseLineSelection('','',function(str) {
494 str = str.replace(/\r/g,'');
495 return str.replace(/(\n|^)[*-]?\s*/g,"$1# ");
496 });
497 }
498 }
499 }
500
501 // spacer
502 jsToolBar.prototype.elements.space3 = {type: 'space'}
503
504 // bq
505 jsToolBar.prototype.elements.bq = {
506 type: 'button',
507 title: 'Quote',
508 fn: {
509 wiki: function() {
510 this.encloseLineSelection('','',function(str) {
511 str = str.replace(/\r/g,'');
512 return str.replace(/(\n|^) *([^\n]*)/g,"$1> $2");
513 });
514 }
515 }
516 }
517
518 // unbq
519 jsToolBar.prototype.elements.unbq = {
520 type: 'button',
521 title: 'Unquote',
522 fn: {
523 wiki: function() {
524 this.encloseLineSelection('','',function(str) {
525 str = str.replace(/\r/g,'');
526 return str.replace(/(\n|^) *[>]? *([^\n]*)/g,"$1$2");
527 });
528 }
529 }
530 }
531
532 // pre
533 jsToolBar.prototype.elements.pre = {
534 type: 'button',
535 title: 'Preformatted text',
536 fn: {
537 wiki: function() { this.encloseLineSelection('<pre>\n', '\n</pre>') }
538 }
539 }
540
541 // spacer
542 jsToolBar.prototype.elements.space4 = {type: 'space'}
543
544 // wiki page
545 jsToolBar.prototype.elements.link = {
546 type: 'button',
547 title: 'Wiki link',
548 fn: {
549 wiki: function() { this.encloseSelection("[[", "]]") }
550 }
551 }
552 // image
553 jsToolBar.prototype.elements.img = {
554 type: 'button',
555 title: 'Image',
556 fn: {
557 wiki: function() { this.encloseSelection("!", "!") }
558 }
559 }
@@ -1,402 +1,409
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,
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
29 29 def setup
30 30 super
31 31 end
32 32
33 33 def test_auto_links
34 34 to_test = {
35 35 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
36 36 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
37 37 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
38 38 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
39 39 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
40 40 '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>.',
41 41 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
42 42 '(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>)',
43 43 '(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>)',
44 44 '(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>).',
45 45 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
46 46 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
47 47 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
48 48 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
49 49 '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>',
50 50 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
51 51 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
52 52 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
53 53 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
54 54 }
55 55 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
56 56 end
57 57
58 58 def test_auto_mailto
59 59 assert_equal '<p><a href="mailto:test@foo.bar" class="email">test@foo.bar</a></p>',
60 60 textilizable('test@foo.bar')
61 61 end
62 62
63 63 def test_inline_images
64 64 to_test = {
65 65 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
66 66 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
67 67 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
68 68 'with style !{width:100px;height100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" style="width:100px;height100px;" alt="" />',
69 69 }
70 70 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
71 71 end
72 72
73 73 def test_textile_external_links
74 74 to_test = {
75 75 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
76 76 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
77 77 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
78 78 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
79 79 # no multiline link text
80 80 "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"
81 81 }
82 82 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
83 83 end
84 84
85 85 def test_redmine_links
86 86 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
87 87 :class => 'issue', :title => 'Error 281 when updating a recipe (New)')
88 88
89 89 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
90 90 :class => 'changeset', :title => 'My very first commit')
91 91
92 92 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
93 93 :class => 'document')
94 94
95 95 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
96 96 :class => 'version')
97 97
98 98 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
99 99
100 100 source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}
101 101 source_url_with_ext = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file.ext']}
102 102
103 103 to_test = {
104 104 # tickets
105 105 '#3, #3 and #3.' => "#{issue_link}, #{issue_link} and #{issue_link}.",
106 106 # changesets
107 107 'r1' => changeset_link,
108 108 # documents
109 109 'document#1' => document_link,
110 110 'document:"Test document"' => document_link,
111 111 # versions
112 112 'version#2' => version_link,
113 113 'version:1.0' => version_link,
114 114 'version:"1.0"' => version_link,
115 115 # source
116 116 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
117 117 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
118 118 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
119 119 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
120 120 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
121 121 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
122 122 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
123 123 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'),
124 124 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
125 125 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'),
126 126 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
127 127 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
128 128 # message
129 129 'message#4' => link_to('Post 2', message_url, :class => 'message'),
130 130 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5'), :class => 'message'),
131 131 # escaping
132 132 '!#3.' => '#3.',
133 133 '!r1' => 'r1',
134 134 '!document#1' => 'document#1',
135 135 '!document:"Test document"' => 'document:"Test document"',
136 136 '!version#2' => 'version#2',
137 137 '!version:1.0' => 'version:1.0',
138 138 '!version:"1.0"' => 'version:"1.0"',
139 139 '!source:/some/file' => 'source:/some/file',
140 140 # invalid expressions
141 141 'source:' => 'source:',
142 142 # url hash
143 143 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
144 144 }
145 145 @project = Project.find(1)
146 146 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
147 147 end
148 148
149 149 def test_wiki_links
150 150 to_test = {
151 151 '[[CookBook documentation]]' => '<a href="/wiki/ecookbook/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
152 152 '[[Another page|Page]]' => '<a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a>',
153 153 # link with anchor
154 154 '[[CookBook documentation#One-section]]' => '<a href="/wiki/ecookbook/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
155 155 '[[Another page#anchor|Page]]' => '<a href="/wiki/ecookbook/Another_page#anchor" class="wiki-page">Page</a>',
156 156 # page that doesn't exist
157 157 '[[Unknown page]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">Unknown page</a>',
158 158 '[[Unknown page|404]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">404</a>',
159 159 # link to another project wiki
160 160 '[[onlinestore:]]' => '<a href="/wiki/onlinestore/" class="wiki-page">onlinestore</a>',
161 161 '[[onlinestore:|Wiki]]' => '<a href="/wiki/onlinestore/" class="wiki-page">Wiki</a>',
162 162 '[[onlinestore:Start page]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Start page</a>',
163 163 '[[onlinestore:Start page|Text]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Text</a>',
164 164 '[[onlinestore:Unknown page]]' => '<a href="/wiki/onlinestore/Unknown_page" class="wiki-page new">Unknown page</a>',
165 165 # striked through link
166 166 '-[[Another page|Page]]-' => '<del><a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a></del>',
167 167 # escaping
168 168 '![[Another page|Page]]' => '[[Another page|Page]]',
169 169 }
170 170 @project = Project.find(1)
171 171 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
172 172 end
173 173
174 174 def test_html_tags
175 175 to_test = {
176 176 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
177 177 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
178 178 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
179 179 # do not escape pre/code tags
180 180 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
181 181 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
182 182 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
183 183 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
184 184 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
185 185 # remove attributes
186 186 "<pre class='foo'>some text</pre>" => "<pre>some text</pre>",
187 187 }
188 188 to_test.each { |text, result| assert_equal result, textilizable(text) }
189 189 end
190 190
191 191 def test_allowed_html_tags
192 192 to_test = {
193 193 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
194 194 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
195 195 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
196 196 }
197 197 to_test.each { |text, result| assert_equal result, textilizable(text) }
198 198 end
199 199
200 200 def test_wiki_links_in_tables
201 201 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
202 202 '<tr><td><a href="/wiki/ecookbook/Page" class="wiki-page new">Link title</a></td>' +
203 203 '<td><a href="/wiki/ecookbook/Other_Page" class="wiki-page new">Other title</a></td>' +
204 204 '</tr><tr><td>Cell 21</td><td><a href="/wiki/ecookbook/Last_page" class="wiki-page new">Last page</a></td></tr>'
205 205 }
206 206 @project = Project.find(1)
207 207 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
208 208 end
209 209
210 210 def test_text_formatting
211 211 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
212 212 '(_text within parentheses_)' => '(<em>text within parentheses</em>)'
213 213 }
214 214 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
215 215 end
216 216
217 217 def test_wiki_horizontal_rule
218 218 assert_equal '<hr />', textilizable('---')
219 219 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
220 220 end
221 221
222 222 def test_footnotes
223 223 raw = <<-RAW
224 224 This is some text[1].
225 225
226 226 fn1. This is the foot note
227 227 RAW
228 228
229 229 expected = <<-EXPECTED
230 230 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
231 231 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
232 232 EXPECTED
233 233
234 234 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
235 235 end
236 236
237 237 def test_table_of_content
238 238 raw = <<-RAW
239 239 {{toc}}
240 240
241 241 h1. Title
242 242
243 243 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
244 244
245 245 h2. Subtitle
246 246
247 247 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
248 248
249 249 h2. Subtitle with %{color:red}red text%
250 250
251 251 h1. Another title
252 252
253 253 RAW
254 254
255 255 expected = '<ul class="toc">' +
256 256 '<li class="heading1"><a href="#Title">Title</a></li>' +
257 257 '<li class="heading2"><a href="#Subtitle">Subtitle</a></li>' +
258 258 '<li class="heading2"><a href="#Subtitle-with-red-text">Subtitle with red text</a></li>' +
259 259 '<li class="heading1"><a href="#Another-title">Another title</a></li>' +
260 260 '</ul>'
261 261
262 262 assert textilizable(raw).gsub("\n", "").include?(expected)
263 263 end
264 264
265 265 def test_blockquote
266 266 # orig raw text
267 267 raw = <<-RAW
268 268 John said:
269 269 > Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
270 270 > Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
271 271 > * Donec odio lorem,
272 272 > * sagittis ac,
273 273 > * malesuada in,
274 274 > * adipiscing eu, dolor.
275 275 >
276 276 > >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
277 277 > Proin a tellus. Nam vel neque.
278 278
279 279 He's right.
280 280 RAW
281 281
282 282 # expected html
283 283 expected = <<-EXPECTED
284 284 <p>John said:</p>
285 285 <blockquote>
286 286 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
287 287 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
288 288 <ul>
289 289 <li>Donec odio lorem,</li>
290 290 <li>sagittis ac,</li>
291 291 <li>malesuada in,</li>
292 292 <li>adipiscing eu, dolor.</li>
293 293 </ul>
294 294 <blockquote>
295 295 <p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
296 296 </blockquote>
297 297 <p>Proin a tellus. Nam vel neque.</p>
298 298 </blockquote>
299 299 <p>He's right.</p>
300 300 EXPECTED
301 301
302 302 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
303 303 end
304 304
305 305 def test_table
306 306 raw = <<-RAW
307 307 This is a table with empty cells:
308 308
309 309 |cell11|cell12||
310 310 |cell21||cell23|
311 311 |cell31|cell32|cell33|
312 312 RAW
313 313
314 314 expected = <<-EXPECTED
315 315 <p>This is a table with empty cells:</p>
316 316
317 317 <table>
318 318 <tr><td>cell11</td><td>cell12</td><td></td></tr>
319 319 <tr><td>cell21</td><td></td><td>cell23</td></tr>
320 320 <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
321 321 </table>
322 322 EXPECTED
323 323
324 324 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
325 325 end
326 326
327 327 def test_macro_hello_world
328 328 text = "{{hello_world}}"
329 329 assert textilizable(text).match(/Hello world!/)
330 330 # escaping
331 331 text = "!{{hello_world}}"
332 332 assert_equal '<p>{{hello_world}}</p>', textilizable(text)
333 333 end
334 334
335 335 def test_macro_include
336 336 @project = Project.find(1)
337 337 # include a page of the current project wiki
338 338 text = "{{include(Another page)}}"
339 339 assert textilizable(text).match(/This is a link to a ticket/)
340 340
341 341 @project = nil
342 342 # include a page of a specific project wiki
343 343 text = "{{include(ecookbook:Another page)}}"
344 344 assert textilizable(text).match(/This is a link to a ticket/)
345 345
346 346 text = "{{include(ecookbook:)}}"
347 347 assert textilizable(text).match(/CookBook documentation/)
348 348
349 349 text = "{{include(unknowidentifier:somepage)}}"
350 350 assert textilizable(text).match(/Unknow project/)
351 351 end
352 352
353 def test_default_formatter
354 Setting.text_formatting = 'unknown'
355 text = 'a *link*: http://www.example.net/'
356 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
357 Setting.text_formatting = 'textile'
358 end
359
353 360 def test_date_format_default
354 361 today = Date.today
355 362 Setting.date_format = ''
356 363 assert_equal l_date(today), format_date(today)
357 364 end
358 365
359 366 def test_date_format
360 367 today = Date.today
361 368 Setting.date_format = '%d %m %Y'
362 369 assert_equal today.strftime('%d %m %Y'), format_date(today)
363 370 end
364 371
365 372 def test_time_format_default
366 373 now = Time.now
367 374 Setting.date_format = ''
368 375 Setting.time_format = ''
369 376 assert_equal l_datetime(now), format_time(now)
370 377 assert_equal l_time(now), format_time(now, false)
371 378 end
372 379
373 380 def test_time_format
374 381 now = Time.now
375 382 Setting.date_format = '%d %m %Y'
376 383 Setting.time_format = '%H %M'
377 384 assert_equal now.strftime('%d %m %Y %H %M'), format_time(now)
378 385 assert_equal now.strftime('%H %M'), format_time(now, false)
379 386 end
380 387
381 388 def test_utc_time_format
382 389 now = Time.now.utc
383 390 Setting.date_format = '%d %m %Y'
384 391 Setting.time_format = '%H %M'
385 392 assert_equal Time.now.strftime('%d %m %Y %H %M'), format_time(now)
386 393 assert_equal Time.now.strftime('%H %M'), format_time(now, false)
387 394 end
388 395
389 396 def test_due_date_distance_in_words
390 397 to_test = { Date.today => 'Due in 0 days',
391 398 Date.today + 1 => 'Due in 1 day',
392 399 Date.today + 100 => 'Due in 100 days',
393 400 Date.today + 20000 => 'Due in 20000 days',
394 401 Date.today - 1 => '1 day late',
395 402 Date.today - 100 => '100 days late',
396 403 Date.today - 20000 => '20000 days late',
397 404 }
398 405 to_test.each do |date, expected|
399 406 assert_equal expected, due_date_distance_in_words(date)
400 407 end
401 408 end
402 409 end
General Comments 0
You need to be logged in to leave comments. Login now