@@ -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\">¶</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 | ||
|
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 | } |
@@ -63,7 +63,7 class WikiController < ApplicationController | |||
|
63 | 63 | @page.content = WikiContent.new(:page => @page) if @page.new_record? |
|
64 | 64 | |
|
65 | 65 | @content = @page.content_for_version(params[:version]) |
|
66 |
@content.text = |
|
|
66 | @content.text = initial_page_content(@page) if @content.text.blank? | |
|
67 | 67 | # don't keep previous comment |
|
68 | 68 | @content.comments = nil |
|
69 | 69 | if request.get? |
@@ -208,4 +208,11 private | |||
|
208 | 208 | def editable?(page = @page) |
|
209 | 209 | page.editable_by?(User.current) |
|
210 | 210 | end |
|
211 | ||
|
212 | # Returns the default content of a new wiki page | |
|
213 | def initial_page_content(page) | |
|
214 | helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting) | |
|
215 | extend helper unless self.instance_of?(helper) | |
|
216 | helper.instance_method(:initial_page_content).bind(self).call(page) | |
|
217 | end | |
|
211 | 218 | end |
@@ -17,10 +17,14 | |||
|
17 | 17 | |
|
18 | 18 | require 'coderay' |
|
19 | 19 | require 'coderay/helpers/file_type' |
|
20 | require 'forwardable' | |
|
20 | 21 | |
|
21 | 22 | module ApplicationHelper |
|
22 | 23 | include Redmine::WikiFormatting::Macros::Definitions |
|
23 | 24 | |
|
25 | extend Forwardable | |
|
26 | def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter | |
|
27 | ||
|
24 | 28 | def current_role |
|
25 | 29 | @current_role ||= User.current.role_for_project(@project) |
|
26 | 30 | end |
@@ -259,9 +263,7 module ApplicationHelper | |||
|
259 | 263 | end |
|
260 | 264 | end |
|
261 | 265 | |
|
262 | text = (Setting.text_formatting == 'textile') ? | |
|
263 | Redmine::WikiFormatting.to_html(text) { |macro, args| exec_macro(macro, obj, args) } : | |
|
264 | simple_format(auto_link(h(text))) | |
|
266 | text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) } | |
|
265 | 267 | |
|
266 | 268 | # different methods for formatting wiki links |
|
267 | 269 | case options[:wiki_links] |
@@ -549,18 +551,6 module ApplicationHelper | |||
|
549 | 551 | end |
|
550 | 552 | end |
|
551 | 553 | |
|
552 | def wikitoolbar_for(field_id) | |
|
553 | return '' unless Setting.text_formatting == 'textile' | |
|
554 | ||
|
555 | help_link = l(:setting_text_formatting) + ': ' + | |
|
556 | link_to(l(:label_help), compute_public_path('wiki_syntax', 'help', 'html'), | |
|
557 | :onclick => "window.open(\"#{ compute_public_path('wiki_syntax', 'help', 'html') }\", \"\", \"resizable=yes, location=no, width=300, height=640, menubar=no, status=no, scrollbars=yes\"); return false;") | |
|
558 | ||
|
559 | javascript_include_tag('jstoolbar/jstoolbar') + | |
|
560 | javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language}") + | |
|
561 | javascript_tag("var toolbar = new jsToolBar($('#{field_id}')); toolbar.setHelpLink('#{help_link}'); toolbar.draw();") | |
|
562 | end | |
|
563 | ||
|
564 | 554 | def content_for(name, content = nil, &block) |
|
565 | 555 | @has_content ||= {} |
|
566 | 556 | @has_content[name] = true |
@@ -570,4 +560,12 module ApplicationHelper | |||
|
570 | 560 | def has_content?(name) |
|
571 | 561 | (@has_content && @has_content[name]) || false |
|
572 | 562 | end |
|
563 | ||
|
564 | private | |
|
565 | ||
|
566 | def wiki_helper | |
|
567 | helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting) | |
|
568 | extend helper | |
|
569 | return self | |
|
570 | end | |
|
573 | 571 | end |
@@ -7,7 +7,7 | |||
|
7 | 7 | <meta name="keywords" content="issue,bug,tracker" /> |
|
8 | 8 | <%= stylesheet_link_tag 'application', :media => 'all' %> |
|
9 | 9 | <%= javascript_include_tag :defaults %> |
|
10 | <%= stylesheet_link_tag 'jstoolbar' %> | |
|
10 | <%= heads_for_wiki_formatter %> | |
|
11 | 11 | <!--[if IE]> |
|
12 | 12 | <style type="text/css"> |
|
13 | 13 | * html body{ width: expression( document.documentElement.clientWidth < 900 ? '900px' : '100%' ); } |
@@ -32,6 +32,6 hr { | |||
|
32 | 32 | <body> |
|
33 | 33 | <%= yield %> |
|
34 | 34 | <hr /> |
|
35 | <span class="footer"><%= Redmine::WikiFormatting.to_html(Setting.emails_footer) %></span> | |
|
35 | <span class="footer"><%= Redmine::WikiFormatting.to_html(Setting.text_formatting, Setting.emails_footer) %></span> | |
|
36 | 36 | </body> |
|
37 | 37 | </html> |
@@ -39,7 +39,7 | |||
|
39 | 39 | <%= select_tag 'settings[protocol]', options_for_select(['http', 'https'], Setting.protocol) %></p> |
|
40 | 40 | |
|
41 | 41 | <p><label><%= l(:setting_text_formatting) %></label> |
|
42 |
<%= select_tag 'settings[text_formatting]', options_for_select([[l(:label_none), "0"], [ |
|
|
42 | <%= select_tag 'settings[text_formatting]', options_for_select([[l(:label_none), "0"], *Redmine::WikiFormatting.format_names.collect{|name| [name, name]} ], Setting.text_formatting.to_sym) %></p> | |
|
43 | 43 | |
|
44 | 44 | <p><label><%= l(:setting_wiki_compression) %></label> |
|
45 | 45 | <%= select_tag 'settings[wiki_compression]', options_for_select( [[l(:label_none), 0], ["gzip", "gzip"]], Setting.wiki_compression) %></p> |
@@ -6,6 +6,7 require 'redmine/core_ext' | |||
|
6 | 6 | require 'redmine/themes' |
|
7 | 7 | require 'redmine/hook' |
|
8 | 8 | require 'redmine/plugin' |
|
9 | require 'redmine/wiki_formatting' | |
|
9 | 10 | |
|
10 | 11 | begin |
|
11 | 12 | require_library_or_gem 'RMagick' unless Object.const_defined?(:Magick) |
@@ -150,3 +151,7 Redmine::Activity.map do |activity| | |||
|
150 | 151 | activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false |
|
151 | 152 | activity.register :messages, :default => false |
|
152 | 153 | end |
|
154 | ||
|
155 | Redmine::WikiFormatting.map do |format| | |
|
156 | format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper | |
|
157 | end |
@@ -149,6 +149,16 module Redmine #:nodoc: | |||
|
149 | 149 | Redmine::Activity.register(*args) |
|
150 | 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 | 162 | # Returns +true+ if the plugin can be configured. |
|
153 | 163 | def configurable? |
|
154 | 164 | settings && settings.is_a?(Hash) && !settings[:partial].blank? |
@@ -1,5 +1,5 | |||
|
1 |
# |
|
|
2 |
# Copyright (C) 2006-200 |
|
|
1 | # Redmine - project management software | |
|
2 | # Copyright (C) 2006-2008 Jean-Philippe Lang | |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
@@ -15,176 +15,65 | |||
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | require 'redcloth3' | |
|
19 | require 'coderay' | |
|
20 | ||
|
21 | 18 | module Redmine |
|
22 | 19 | module WikiFormatting |
|
20 | @@formatters = {} | |
|
23 | 21 | |
|
24 | private | |
|
25 | ||
|
26 | class TextileFormatter < RedCloth3 | |
|
27 | ||
|
28 | # auto_link rule after textile rules so that it doesn't break !image_url! tags | |
|
29 | RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto, :inline_toc, :inline_macros] | |
|
30 | ||
|
31 | def initialize(*args) | |
|
32 | super | |
|
33 | self.hard_breaks=true | |
|
34 | self.no_span_caps=true | |
|
22 | class << self | |
|
23 | def map | |
|
24 | yield self | |
|
35 | 25 | end |
|
36 | 26 | |
|
37 | def to_html(*rules, &block) | |
|
38 | @toc = [] | |
|
39 | @macros_runner = block | |
|
40 | super(*RULES).to_s | |
|
27 | def register(name, formatter, helper) | |
|
28 | raise ArgumentError, "format name '#{name}' is already taken" if @@formatters[name] | |
|
29 | @@formatters[name.to_sym] = {:formatter => formatter, :helper => helper} | |
|
41 | 30 | end |
|
42 | 31 | |
|
43 | private | |
|
44 | ||
|
45 | # Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet. | |
|
46 | # <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a> | |
|
47 | def hard_break( text ) | |
|
48 | text.gsub!( /(.)\n(?!\n|\Z|>| *(>? *[#*=]+(\s|$)|[{|]))/, "\\1<br />\n" ) if hard_breaks | |
|
32 | def formatter_for(name) | |
|
33 | entry = @@formatters[name.to_sym] | |
|
34 | (entry && entry[:formatter]) || Redmine::WikiFormatting::NullFormatter::Formatter | |
|
49 | 35 | end |
|
50 | 36 | |
|
51 | # Patch to add code highlighting support to RedCloth | |
|
52 | def smooth_offtags( text ) | |
|
53 | unless @pre_list.empty? | |
|
54 | ## replace <pre> content | |
|
55 | text.gsub!(/<redpre#(\d+)>/) do | |
|
56 | content = @pre_list[$1.to_i] | |
|
57 | if content.match(/<code\s+class="(\w+)">\s?(.+)/m) | |
|
58 | content = "<code class=\"#{$1} CodeRay\">" + | |
|
59 | CodeRay.scan($2, $1.downcase).html(:escape => false, :line_numbers => :inline) | |
|
60 | end | |
|
61 | content | |
|
37 | def helper_for(name) | |
|
38 | entry = @@formatters[name.to_sym] | |
|
39 | (entry && entry[:helper]) || Redmine::WikiFormatting::NullFormatter::Helper | |
|
62 | 40 |
|
|
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 | |
|
75 | anchor = toc_item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') | |
|
76 | ||
|
77 | unless anchor.blank? | |
|
78 | if tag =~ /^h(\d)$/ | |
|
79 | @toc << [$1.to_i, anchor, toc_item] | |
|
80 | end | |
|
81 | atts << " id=\"#{anchor}\"" | |
|
82 | content = content + "<a href=\"##{anchor}\" class=\"wiki-anchor\">¶</a>" | |
|
83 | end | |
|
84 | textile_p(tag, atts, cite, content) | |
|
42 | def format_names | |
|
43 | @@formatters.keys.map | |
|
85 | 44 | end |
|
86 | 45 | |
|
87 | alias :textile_h1 :textile_p_withtoc | |
|
88 | alias :textile_h2 :textile_p_withtoc | |
|
89 | alias :textile_h3 :textile_p_withtoc | |
|
90 | ||
|
91 | def inline_toc(text) | |
|
92 | text.gsub!(/<p>\{\{([<>]?)toc\}\}<\/p>/i) do | |
|
93 | div_class = 'toc' | |
|
94 | div_class << ' right' if $1 == '>' | |
|
95 | div_class << ' left' if $1 == '<' | |
|
96 | out = "<ul class=\"#{div_class}\">" | |
|
97 | @toc.each do |heading| | |
|
98 | level, anchor, toc_item = heading | |
|
99 | out << "<li class=\"heading#{level}\"><a href=\"##{anchor}\">#{toc_item}</a></li>\n" | |
|
100 | end | |
|
101 | out << '</ul>' | |
|
102 | out | |
|
46 | def to_html(format, text, options = {}, &block) | |
|
47 | formatter_for(format).new(text).to_html(&block) | |
|
103 | 48 |
|
|
104 | 49 |
|
|
105 | 50 |
|
|
106 | MACROS_RE = / | |
|
107 | (!)? # escaping | |
|
108 | ( | |
|
109 | \{\{ # opening tag | |
|
110 | ([\w]+) # macro name | |
|
111 | (\(([^\}]*)\))? # optional arguments | |
|
112 | \}\} # closing tag | |
|
113 | ) | |
|
114 | /x unless const_defined?(:MACROS_RE) | |
|
51 | # Default formatter module | |
|
52 | module NullFormatter | |
|
53 | class Formatter | |
|
54 | include ActionView::Helpers::TagHelper | |
|
55 | include ActionView::Helpers::TextHelper | |
|
115 | 56 | |
|
116 |
def in |
|
|
117 | text.gsub!(MACROS_RE) do | |
|
118 | esc, all, macro = $1, $2, $3.downcase | |
|
119 | args = ($5 || '').split(',').each(&:strip) | |
|
120 | if esc.nil? | |
|
121 | begin | |
|
122 | @macros_runner.call(macro, args) | |
|
123 | rescue => e | |
|
124 | "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>" | |
|
125 | end || all | |
|
126 | else | |
|
127 | all | |
|
128 | end | |
|
57 | def initialize(text) | |
|
58 | @text = text | |
|
129 | 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). | |
|
152 | def inline_auto_link(text) | |
|
153 | text.gsub!(AUTO_LINK_RE) do | |
|
154 | all, leading, proto, url, post = $&, $1, $2, $3, $6 | |
|
155 | if leading =~ /<a\s/i || leading =~ /![<>=]?/ | |
|
156 | # don't replace URL's that are already linked | |
|
157 | # and URL's prefixed with ! !> !< != (textile images) | |
|
158 | all | |
|
159 | else | |
|
160 | # Idea below : an URL with unbalanced parethesis and | |
|
161 | # ending by ')' is put into external parenthesis | |
|
162 | if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) ) | |
|
163 | url=url[0..-2] # discard closing parenth from url | |
|
164 | post = ")"+post # add closing parenth to post | |
|
165 | end | |
|
166 | %(#{leading}<a class="external" href="#{proto=="www."?"http://www.":proto}#{url}">#{proto + url}</a>#{post}) | |
|
167 | end | |
|
61 | def to_html(*args) | |
|
62 | simple_format(auto_link(CGI::escapeHTML(@text))) | |
|
168 | 63 | end |
|
169 | 64 | end |
|
170 | 65 | |
|
171 | # Turns all email addresses into clickable links (code from Rails). | |
|
172 | def inline_auto_mailto(text) | |
|
173 | text.gsub!(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do | |
|
174 | mail = $1 | |
|
175 | if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/) | |
|
176 | ||
|
177 | else | |
|
178 | %{<a href="mailto:#{mail}" class="email">#{mail}</a>} | |
|
66 | module Helper | |
|
67 | def wikitoolbar_for(field_id) | |
|
179 | 68 |
|
|
69 | ||
|
70 | def heads_for_wiki_formatter | |
|
180 | 71 | end |
|
72 | ||
|
73 | def initial_page_content(page) | |
|
74 | page.pretty_title.to_s | |
|
181 | 75 | end |
|
182 | 76 | end |
|
183 | ||
|
184 | public | |
|
185 | ||
|
186 | def self.to_html(text, options = {}, &block) | |
|
187 | TextileFormatter.new(text).to_html(&block) | |
|
188 | 77 | end |
|
189 | 78 | end |
|
190 | 79 | end |
@@ -378,182 +378,3 jsToolBar.prototype.resizeDragStop = function(event) { | |||
|
378 | 378 | document.removeEventListener('mousemove', this.dragMoveHdlr, false); |
|
379 | 379 | document.removeEventListener('mouseup', this.dragStopHdlr, false); |
|
380 | 380 | }; |
|
381 | ||
|
382 | // Elements definition ------------------------------------ | |
|
383 | ||
|
384 | // strong | |
|
385 | jsToolBar.prototype.elements.strong = { | |
|
386 | type: 'button', | |
|
387 | title: 'Strong', | |
|
388 | fn: { | |
|
389 | wiki: function() { this.singleTag('*') } | |
|
390 | } | |
|
391 | } | |
|
392 | ||
|
393 | // em | |
|
394 | jsToolBar.prototype.elements.em = { | |
|
395 | type: 'button', | |
|
396 | title: 'Italic', | |
|
397 | fn: { | |
|
398 | wiki: function() { this.singleTag("_") } | |
|
399 | } | |
|
400 | } | |
|
401 | ||
|
402 | // ins | |
|
403 | jsToolBar.prototype.elements.ins = { | |
|
404 | type: 'button', | |
|
405 | title: 'Underline', | |
|
406 | fn: { | |
|
407 | wiki: function() { this.singleTag('+') } | |
|
408 | } | |
|
409 | } | |
|
410 | ||
|
411 | // del | |
|
412 | jsToolBar.prototype.elements.del = { | |
|
413 | type: 'button', | |
|
414 | title: 'Deleted', | |
|
415 | fn: { | |
|
416 | wiki: function() { this.singleTag('-') } | |
|
417 | } | |
|
418 | } | |
|
419 | ||
|
420 | // code | |
|
421 | jsToolBar.prototype.elements.code = { | |
|
422 | type: 'button', | |
|
423 | title: 'Code', | |
|
424 | fn: { | |
|
425 | wiki: function() { this.singleTag('@') } | |
|
426 | } | |
|
427 | } | |
|
428 | ||
|
429 | // spacer | |
|
430 | jsToolBar.prototype.elements.space1 = {type: 'space'} | |
|
431 | ||
|
432 | // headings | |
|
433 | jsToolBar.prototype.elements.h1 = { | |
|
434 | type: 'button', | |
|
435 | title: 'Heading 1', | |
|
436 | fn: { | |
|
437 | wiki: function() { | |
|
438 | this.encloseLineSelection('h1. ', '',function(str) { | |
|
439 | str = str.replace(/^h\d+\.\s+/, '') | |
|
440 | return str; | |
|
441 | }); | |
|
442 | } | |
|
443 | } | |
|
444 | } | |
|
445 | jsToolBar.prototype.elements.h2 = { | |
|
446 | type: 'button', | |
|
447 | title: 'Heading 2', | |
|
448 | fn: { | |
|
449 | wiki: function() { | |
|
450 | this.encloseLineSelection('h2. ', '',function(str) { | |
|
451 | str = str.replace(/^h\d+\.\s+/, '') | |
|
452 | return str; | |
|
453 | }); | |
|
454 | } | |
|
455 | } | |
|
456 | } | |
|
457 | jsToolBar.prototype.elements.h3 = { | |
|
458 | type: 'button', | |
|
459 | title: 'Heading 3', | |
|
460 | fn: { | |
|
461 | wiki: function() { | |
|
462 | this.encloseLineSelection('h3. ', '',function(str) { | |
|
463 | str = str.replace(/^h\d+\.\s+/, '') | |
|
464 | return str; | |
|
465 | }); | |
|
466 | } | |
|
467 | } | |
|
468 | } | |
|
469 | ||
|
470 | // spacer | |
|
471 | jsToolBar.prototype.elements.space2 = {type: 'space'} | |
|
472 | ||
|
473 | // ul | |
|
474 | jsToolBar.prototype.elements.ul = { | |
|
475 | type: 'button', | |
|
476 | title: 'Unordered list', | |
|
477 | fn: { | |
|
478 | wiki: function() { | |
|
479 | this.encloseLineSelection('','',function(str) { | |
|
480 | str = str.replace(/\r/g,''); | |
|
481 | return str.replace(/(\n|^)[#-]?\s*/g,"$1* "); | |
|
482 | }); | |
|
483 | } | |
|
484 | } | |
|
485 | } | |
|
486 | ||
|
487 | // ol | |
|
488 | jsToolBar.prototype.elements.ol = { | |
|
489 | type: 'button', | |
|
490 | title: 'Ordered list', | |
|
491 | fn: { | |
|
492 | wiki: function() { | |
|
493 | this.encloseLineSelection('','',function(str) { | |
|
494 | str = str.replace(/\r/g,''); | |
|
495 | return str.replace(/(\n|^)[*-]?\s*/g,"$1# "); | |
|
496 | }); | |
|
497 | } | |
|
498 | } | |
|
499 | } | |
|
500 | ||
|
501 | // spacer | |
|
502 | jsToolBar.prototype.elements.space3 = {type: 'space'} | |
|
503 | ||
|
504 | // bq | |
|
505 | jsToolBar.prototype.elements.bq = { | |
|
506 | type: 'button', | |
|
507 | title: 'Quote', | |
|
508 | fn: { | |
|
509 | wiki: function() { | |
|
510 | this.encloseLineSelection('','',function(str) { | |
|
511 | str = str.replace(/\r/g,''); | |
|
512 | return str.replace(/(\n|^) *([^\n]*)/g,"$1> $2"); | |
|
513 | }); | |
|
514 | } | |
|
515 | } | |
|
516 | } | |
|
517 | ||
|
518 | // unbq | |
|
519 | jsToolBar.prototype.elements.unbq = { | |
|
520 | type: 'button', | |
|
521 | title: 'Unquote', | |
|
522 | fn: { | |
|
523 | wiki: function() { | |
|
524 | this.encloseLineSelection('','',function(str) { | |
|
525 | str = str.replace(/\r/g,''); | |
|
526 | return str.replace(/(\n|^) *[>]? *([^\n]*)/g,"$1$2"); | |
|
527 | }); | |
|
528 | } | |
|
529 | } | |
|
530 | } | |
|
531 | ||
|
532 | // pre | |
|
533 | jsToolBar.prototype.elements.pre = { | |
|
534 | type: 'button', | |
|
535 | title: 'Preformatted text', | |
|
536 | fn: { | |
|
537 | wiki: function() { this.encloseLineSelection('<pre>\n', '\n</pre>') } | |
|
538 | } | |
|
539 | } | |
|
540 | ||
|
541 | // spacer | |
|
542 | jsToolBar.prototype.elements.space4 = {type: 'space'} | |
|
543 | ||
|
544 | // wiki page | |
|
545 | jsToolBar.prototype.elements.link = { | |
|
546 | type: 'button', | |
|
547 | title: 'Wiki link', | |
|
548 | fn: { | |
|
549 | wiki: function() { this.encloseSelection("[[", "]]") } | |
|
550 | } | |
|
551 | } | |
|
552 | // image | |
|
553 | jsToolBar.prototype.elements.img = { | |
|
554 | type: 'button', | |
|
555 | title: 'Image', | |
|
556 | fn: { | |
|
557 | wiki: function() { this.encloseSelection("!", "!") } | |
|
558 | } | |
|
559 | } |
@@ -350,6 +350,13 EXPECTED | |||
|
350 | 350 | assert textilizable(text).match(/Unknow project/) |
|
351 | 351 | end |
|
352 | 352 | |
|
353 | def test_default_formatter | |
|
354 | Setting.text_formatting = 'unknown' | |
|
355 | text = 'a *link*: http://www.example.net/' | |
|
356 | assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text) | |
|
357 | Setting.text_formatting = 'textile' | |
|
358 | end | |
|
359 | ||
|
353 | 360 | def test_date_format_default |
|
354 | 361 | today = Date.today |
|
355 | 362 | Setting.date_format = '' |
General Comments 0
You need to be logged in to leave comments.
Login now