##// END OF EJS Templates
Fixed: TOC does not remove colorization markups (#1423)....
Jean-Philippe Lang -
r1528:11e9891425c4
parent child
Show More
@@ -1,168 +1,171
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require 'redcloth'
19 19 require 'coderay'
20 20
21 21 module Redmine
22 22 module WikiFormatting
23 23
24 24 private
25 25
26 26 class TextileFormatter < RedCloth
27 27
28 28 # auto_link rule after textile rules so that it doesn't break !image_url! tags
29 29 RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto, :inline_toc, :inline_macros]
30 30
31 31 def initialize(*args)
32 32 super
33 33 self.hard_breaks=true
34 34 self.no_span_caps=true
35 35 end
36 36
37 37 def to_html(*rules, &block)
38 38 @toc = []
39 39 @macros_runner = block
40 40 super(*RULES).to_s
41 41 end
42 42
43 43 private
44 44
45 45 # Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet.
46 46 # <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a>
47 47 def hard_break( text )
48 48 text.gsub!( /(.)\n(?!\n|\Z|>| *(>? *[#*=]+(\s|$)|[{|]))/, "\\1<br />\n" ) if hard_breaks
49 49 end
50 50
51 51 # Patch to add code highlighting support to RedCloth
52 52 def smooth_offtags( text )
53 53 unless @pre_list.empty?
54 54 ## replace <pre> content
55 55 text.gsub!(/<redpre#(\d+)>/) do
56 56 content = @pre_list[$1.to_i]
57 57 if content.match(/<code\s+class="(\w+)">\s?(.+)/m)
58 58 content = "<code class=\"#{$1} CodeRay\">" +
59 59 CodeRay.scan($2, $1.downcase).html(:escape => false, :line_numbers => :inline)
60 60 end
61 61 content
62 62 end
63 63 end
64 64 end
65 65
66 66 # Patch to add 'table of content' support to RedCloth
67 67 def textile_p_withtoc(tag, atts, cite, content)
68 68 if tag =~ /^h(\d)$/
69 69 @toc << [$1.to_i, content]
70 70 end
71 71 content = "<a name=\"#{@toc.length}\" class=\"wiki-page\"></a>" + content
72 72 textile_p(tag, atts, cite, content)
73 73 end
74 74
75 75 alias :textile_h1 :textile_p_withtoc
76 76 alias :textile_h2 :textile_p_withtoc
77 77 alias :textile_h3 :textile_p_withtoc
78 78
79 79 def inline_toc(text)
80 80 text.gsub!(/<p>\{\{([<>]?)toc\}\}<\/p>/i) do
81 81 div_class = 'toc'
82 82 div_class << ' right' if $1 == '>'
83 83 div_class << ' left' if $1 == '<'
84 84 out = "<div class=\"#{div_class}\">"
85 85 @toc.each_with_index do |heading, index|
86 86 # remove wiki links from the item
87 87 toc_item = heading.last.gsub(/(\[\[|\]\])/, '')
88 # remove styles
89 # eg. %{color:red}Triggers% => Triggers
90 toc_item.gsub! %r[%\{[^\}]*\}([^%]+)%], '\\1'
88 91 out << "<a href=\"##{index+1}\" class=\"heading#{heading.first}\">#{toc_item}</a>"
89 92 end
90 93 out << '</div>'
91 94 out
92 95 end
93 96 end
94 97
95 98 MACROS_RE = /
96 99 (!)? # escaping
97 100 (
98 101 \{\{ # opening tag
99 102 ([\w]+) # macro name
100 103 (\(([^\}]*)\))? # optional arguments
101 104 \}\} # closing tag
102 105 )
103 106 /x unless const_defined?(:MACROS_RE)
104 107
105 108 def inline_macros(text)
106 109 text.gsub!(MACROS_RE) do
107 110 esc, all, macro = $1, $2, $3.downcase
108 111 args = ($5 || '').split(',').each(&:strip)
109 112 if esc.nil?
110 113 begin
111 114 @macros_runner.call(macro, args)
112 115 rescue => e
113 116 "<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
114 117 end || all
115 118 else
116 119 all
117 120 end
118 121 end
119 122 end
120 123
121 124 AUTO_LINK_RE = %r{
122 125 ( # leading text
123 126 <\w+.*?>| # leading HTML tag, or
124 127 [^=<>!:'"/]| # leading punctuation, or
125 128 ^ # beginning of line
126 129 )
127 130 (
128 131 (?:https?://)| # protocol spec, or
129 132 (?:www\.) # www.*
130 133 )
131 134 (
132 135 (\S+?) # url
133 136 (\/)? # slash
134 137 )
135 138 ([^\w\=\/;]*?) # post
136 139 (?=<|\s|$)
137 140 }x unless const_defined?(:AUTO_LINK_RE)
138 141
139 142 # Turns all urls into clickable links (code from Rails).
140 143 def inline_auto_link(text)
141 144 text.gsub!(AUTO_LINK_RE) do
142 145 all, leading, proto, url, post = $&, $1, $2, $3, $6
143 146 if leading =~ /<a\s/i || leading =~ /![<>=]?/
144 147 # don't replace URL's that are already linked
145 148 # and URL's prefixed with ! !> !< != (textile images)
146 149 all
147 150 else
148 151 %(#{leading}<a class="external" href="#{proto=="www."?"http://www.":proto}#{url}">#{proto + url}</a>#{post})
149 152 end
150 153 end
151 154 end
152 155
153 156 # Turns all email addresses into clickable links (code from Rails).
154 157 def inline_auto_mailto(text)
155 158 text.gsub!(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
156 159 text = $1
157 160 %{<a href="mailto:#{$1}" class="email">#{text}</a>}
158 161 end
159 162 end
160 163 end
161 164
162 165 public
163 166
164 167 def self.to_html(text, options = {}, &block)
165 168 TextileFormatter.new(text).to_html(&block)
166 169 end
167 170 end
168 171 end
@@ -1,301 +1,329
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../../test_helper'
19 19
20 20 class ApplicationHelperTest < HelperTestCase
21 21 include ApplicationHelper
22 22 include ActionView::Helpers::TextHelper
23 23 fixtures :projects, :repositories, :changesets, :trackers, :issue_statuses, :issues, :documents, :versions, :wikis, :wiki_pages, :wiki_contents, :roles, :enabled_modules
24 24
25 25 def setup
26 26 super
27 27 end
28 28
29 29 def test_auto_links
30 30 to_test = {
31 31 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
32 32 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
33 33 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
34 34 '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>.',
35 35 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
36 36 '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>',
37 37 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>'
38 38 }
39 39 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
40 40 end
41 41
42 42 def test_auto_mailto
43 43 assert_equal '<p><a href="mailto:test@foo.bar" class="email">test@foo.bar</a></p>',
44 44 textilizable('test@foo.bar')
45 45 end
46 46
47 47 def test_inline_images
48 48 to_test = {
49 49 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
50 50 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
51 51 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
52 52 'with style !{width:100px;height100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" style="width:100px;height100px;" alt="" />',
53 53 }
54 54 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
55 55 end
56 56
57 57 def test_textile_external_links
58 58 to_test = {
59 59 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
60 60 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
61 61 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
62 62 # no multiline link text
63 63 "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"
64 64 }
65 65 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
66 66 end
67 67
68 68 def test_redmine_links
69 69 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
70 70 :class => 'issue', :title => 'Error 281 when updating a recipe (New)')
71 71
72 72 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
73 73 :class => 'changeset', :title => 'My very first commit')
74 74
75 75 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
76 76 :class => 'document')
77 77
78 78 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
79 79 :class => 'version')
80 80
81 81 source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => 'some/file'}
82 82
83 83 to_test = {
84 84 # tickets
85 85 '#3, #3 and #3.' => "#{issue_link}, #{issue_link} and #{issue_link}.",
86 86 # changesets
87 87 'r1' => changeset_link,
88 88 # documents
89 89 'document#1' => document_link,
90 90 'document:"Test document"' => document_link,
91 91 # versions
92 92 'version#2' => version_link,
93 93 'version:1.0' => version_link,
94 94 'version:"1.0"' => version_link,
95 95 # source
96 96 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
97 97 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
98 98 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
99 99 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
100 100 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
101 101 # escaping
102 102 '!#3.' => '#3.',
103 103 '!r1' => 'r1',
104 104 '!document#1' => 'document#1',
105 105 '!document:"Test document"' => 'document:"Test document"',
106 106 '!version#2' => 'version#2',
107 107 '!version:1.0' => 'version:1.0',
108 108 '!version:"1.0"' => 'version:"1.0"',
109 109 '!source:/some/file' => 'source:/some/file',
110 110 # invalid expressions
111 111 'source:' => 'source:',
112 112 # url hash
113 113 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
114 114 }
115 115 @project = Project.find(1)
116 116 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
117 117 end
118 118
119 119 def test_wiki_links
120 120 to_test = {
121 121 '[[CookBook documentation]]' => '<a href="/wiki/ecookbook/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
122 122 '[[Another page|Page]]' => '<a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a>',
123 123 # page that doesn't exist
124 124 '[[Unknown page]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">Unknown page</a>',
125 125 '[[Unknown page|404]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">404</a>',
126 126 # link to another project wiki
127 127 '[[onlinestore:]]' => '<a href="/wiki/onlinestore/" class="wiki-page">onlinestore</a>',
128 128 '[[onlinestore:|Wiki]]' => '<a href="/wiki/onlinestore/" class="wiki-page">Wiki</a>',
129 129 '[[onlinestore:Start page]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Start page</a>',
130 130 '[[onlinestore:Start page|Text]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Text</a>',
131 131 '[[onlinestore:Unknown page]]' => '<a href="/wiki/onlinestore/Unknown_page" class="wiki-page new">Unknown page</a>',
132 132 # striked through link
133 133 '-[[Another page|Page]]-' => '<del><a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a></del>',
134 134 # escaping
135 135 '![[Another page|Page]]' => '[[Another page|Page]]',
136 136 }
137 137 @project = Project.find(1)
138 138 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
139 139 end
140 140
141 141 def test_html_tags
142 142 to_test = {
143 143 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
144 144 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
145 145 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
146 146 # do not escape pre/code tags
147 147 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
148 148 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
149 149 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
150 150 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
151 151 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>"
152 152 }
153 153 to_test.each { |text, result| assert_equal result, textilizable(text) }
154 154 end
155 155
156 156 def test_allowed_html_tags
157 157 to_test = {
158 158 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
159 159 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
160 160 }
161 161 to_test.each { |text, result| assert_equal result, textilizable(text) }
162 162 end
163 163
164 164 def test_wiki_links_in_tables
165 165 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
166 166 '<tr><td><a href="/wiki/ecookbook/Page" class="wiki-page new">Link title</a></td>' +
167 167 '<td><a href="/wiki/ecookbook/Other_Page" class="wiki-page new">Other title</a></td>' +
168 168 '</tr><tr><td>Cell 21</td><td><a href="/wiki/ecookbook/Last_page" class="wiki-page new">Last page</a></td></tr>'
169 169 }
170 170 @project = Project.find(1)
171 171 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
172 172 end
173 173
174 174 def test_text_formatting
175 175 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
176 176 '(_text within parentheses_)' => '(<em>text within parentheses</em>)'
177 177 }
178 178 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
179 179 end
180 180
181 181 def test_wiki_horizontal_rule
182 182 assert_equal '<hr />', textilizable('---')
183 183 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
184 184 end
185 185
186 def test_table_of_content
187 raw = <<-RAW
188 {{toc}}
189
190 h1. Title
191
192 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
193
194 h2. Subtitle
195
196 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
197
198 h2. Subtitle with %{color:red}red text%
199
200 h1. Another title
201
202 RAW
203
204 expected = '<div class="toc">' +
205 '<a href="#1" class="heading1">Title</a>' +
206 '<a href="#2" class="heading2">Subtitle</a>' +
207 '<a href="#3" class="heading2">Subtitle with red text</a>' +
208 '<a href="#4" class="heading1">Another title</a>' +
209 '</div>'
210
211 assert textilizable(raw).include?(expected)
212 end
213
186 214 def test_blockquote
187 215 # orig raw text
188 216 raw = <<-RAW
189 217 John said:
190 218 > Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
191 219 > Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
192 220 > * Donec odio lorem,
193 221 > * sagittis ac,
194 222 > * malesuada in,
195 223 > * adipiscing eu, dolor.
196 224 >
197 225 > >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
198 226 > Proin a tellus. Nam vel neque.
199 227
200 228 He's right.
201 229 RAW
202 230
203 231 # expected html
204 232 expected = <<-EXPECTED
205 233 <p>John said:</p>
206 234 <blockquote>
207 235 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
208 236 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
209 237 <ul>
210 238 <li>Donec odio lorem,</li>
211 239 <li>sagittis ac,</li>
212 240 <li>malesuada in,</li>
213 241 <li>adipiscing eu, dolor.</li>
214 242 </ul>
215 243 <blockquote>
216 244 <p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
217 245 </blockquote>
218 246 <p>Proin a tellus. Nam vel neque.</p>
219 247 </blockquote>
220 248 <p>He's right.</p>
221 249 EXPECTED
222 250
223 251 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
224 252 end
225 253
226 254 def test_table
227 255 raw = <<-RAW
228 256 This is a table with empty cells:
229 257
230 258 |cell11|cell12||
231 259 |cell21||cell23|
232 260 |cell31|cell32|cell33|
233 261 RAW
234 262
235 263 expected = <<-EXPECTED
236 264 <p>This is a table with empty cells:</p>
237 265
238 266 <table>
239 267 <tr><td>cell11</td><td>cell12</td><td></td></tr>
240 268 <tr><td>cell21</td><td></td><td>cell23</td></tr>
241 269 <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
242 270 </table>
243 271 EXPECTED
244 272
245 273 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
246 274 end
247 275
248 276 def test_macro_hello_world
249 277 text = "{{hello_world}}"
250 278 assert textilizable(text).match(/Hello world!/)
251 279 # escaping
252 280 text = "!{{hello_world}}"
253 281 assert_equal '<p>{{hello_world}}</p>', textilizable(text)
254 282 end
255 283
256 284 def test_macro_include
257 285 @project = Project.find(1)
258 286 # include a page of the current project wiki
259 287 text = "{{include(Another page)}}"
260 288 assert textilizable(text).match(/This is a link to a ticket/)
261 289
262 290 @project = nil
263 291 # include a page of a specific project wiki
264 292 text = "{{include(ecookbook:Another page)}}"
265 293 assert textilizable(text).match(/This is a link to a ticket/)
266 294
267 295 text = "{{include(ecookbook:)}}"
268 296 assert textilizable(text).match(/CookBook documentation/)
269 297
270 298 text = "{{include(unknowidentifier:somepage)}}"
271 299 assert textilizable(text).match(/Unknow project/)
272 300 end
273 301
274 302 def test_date_format_default
275 303 today = Date.today
276 304 Setting.date_format = ''
277 305 assert_equal l_date(today), format_date(today)
278 306 end
279 307
280 308 def test_date_format
281 309 today = Date.today
282 310 Setting.date_format = '%d %m %Y'
283 311 assert_equal today.strftime('%d %m %Y'), format_date(today)
284 312 end
285 313
286 314 def test_time_format_default
287 315 now = Time.now
288 316 Setting.date_format = ''
289 317 Setting.time_format = ''
290 318 assert_equal l_datetime(now), format_time(now)
291 319 assert_equal l_time(now), format_time(now, false)
292 320 end
293 321
294 322 def test_time_format
295 323 now = Time.now
296 324 Setting.date_format = '%d %m %Y'
297 325 Setting.time_format = '%H %M'
298 326 assert_equal now.strftime('%d %m %Y %H %M'), format_time(now)
299 327 assert_equal now.strftime('%H %M'), format_time(now, false)
300 328 end
301 329 end
General Comments 0
You need to be logged in to leave comments. Login now