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