##// END OF EJS Templates
Merged r15442 (#22898)....
Jean-Philippe Lang -
r15062:cdfebdcec8d9
parent child
Show More
@@ -1,1211 +1,1211
1 # vim:ts=4:sw=4:
1 # vim:ts=4:sw=4:
2 # = RedCloth - Textile and Markdown Hybrid for Ruby
2 # = RedCloth - Textile and Markdown Hybrid for Ruby
3 #
3 #
4 # Homepage:: http://whytheluckystiff.net/ruby/redcloth/
4 # Homepage:: http://whytheluckystiff.net/ruby/redcloth/
5 # Author:: why the lucky stiff (http://whytheluckystiff.net/)
5 # Author:: why the lucky stiff (http://whytheluckystiff.net/)
6 # Copyright:: (cc) 2004 why the lucky stiff (and his puppet organizations.)
6 # Copyright:: (cc) 2004 why the lucky stiff (and his puppet organizations.)
7 # License:: BSD
7 # License:: BSD
8 #
8 #
9 # (see http://hobix.com/textile/ for a Textile Reference.)
9 # (see http://hobix.com/textile/ for a Textile Reference.)
10 #
10 #
11 # Based on (and also inspired by) both:
11 # Based on (and also inspired by) both:
12 #
12 #
13 # PyTextile: http://diveintomark.org/projects/textile/textile.py.txt
13 # PyTextile: http://diveintomark.org/projects/textile/textile.py.txt
14 # Textism for PHP: http://www.textism.com/tools/textile/
14 # Textism for PHP: http://www.textism.com/tools/textile/
15 #
15 #
16 #
16 #
17
17
18 # = RedCloth
18 # = RedCloth
19 #
19 #
20 # RedCloth is a Ruby library for converting Textile and/or Markdown
20 # RedCloth is a Ruby library for converting Textile and/or Markdown
21 # into HTML. You can use either format, intermingled or separately.
21 # into HTML. You can use either format, intermingled or separately.
22 # You can also extend RedCloth to honor your own custom text stylings.
22 # You can also extend RedCloth to honor your own custom text stylings.
23 #
23 #
24 # RedCloth users are encouraged to use Textile if they are generating
24 # RedCloth users are encouraged to use Textile if they are generating
25 # HTML and to use Markdown if others will be viewing the plain text.
25 # HTML and to use Markdown if others will be viewing the plain text.
26 #
26 #
27 # == What is Textile?
27 # == What is Textile?
28 #
28 #
29 # Textile is a simple formatting style for text
29 # Textile is a simple formatting style for text
30 # documents, loosely based on some HTML conventions.
30 # documents, loosely based on some HTML conventions.
31 #
31 #
32 # == Sample Textile Text
32 # == Sample Textile Text
33 #
33 #
34 # h2. This is a title
34 # h2. This is a title
35 #
35 #
36 # h3. This is a subhead
36 # h3. This is a subhead
37 #
37 #
38 # This is a bit of paragraph.
38 # This is a bit of paragraph.
39 #
39 #
40 # bq. This is a blockquote.
40 # bq. This is a blockquote.
41 #
41 #
42 # = Writing Textile
42 # = Writing Textile
43 #
43 #
44 # A Textile document consists of paragraphs. Paragraphs
44 # A Textile document consists of paragraphs. Paragraphs
45 # can be specially formatted by adding a small instruction
45 # can be specially formatted by adding a small instruction
46 # to the beginning of the paragraph.
46 # to the beginning of the paragraph.
47 #
47 #
48 # h[n]. Header of size [n].
48 # h[n]. Header of size [n].
49 # bq. Blockquote.
49 # bq. Blockquote.
50 # # Numeric list.
50 # # Numeric list.
51 # * Bulleted list.
51 # * Bulleted list.
52 #
52 #
53 # == Quick Phrase Modifiers
53 # == Quick Phrase Modifiers
54 #
54 #
55 # Quick phrase modifiers are also included, to allow formatting
55 # Quick phrase modifiers are also included, to allow formatting
56 # of small portions of text within a paragraph.
56 # of small portions of text within a paragraph.
57 #
57 #
58 # \_emphasis\_
58 # \_emphasis\_
59 # \_\_italicized\_\_
59 # \_\_italicized\_\_
60 # \*strong\*
60 # \*strong\*
61 # \*\*bold\*\*
61 # \*\*bold\*\*
62 # ??citation??
62 # ??citation??
63 # -deleted text-
63 # -deleted text-
64 # +inserted text+
64 # +inserted text+
65 # ^superscript^
65 # ^superscript^
66 # ~subscript~
66 # ~subscript~
67 # @code@
67 # @code@
68 # %(classname)span%
68 # %(classname)span%
69 #
69 #
70 # ==notextile== (leave text alone)
70 # ==notextile== (leave text alone)
71 #
71 #
72 # == Links
72 # == Links
73 #
73 #
74 # To make a hypertext link, put the link text in "quotation
74 # To make a hypertext link, put the link text in "quotation
75 # marks" followed immediately by a colon and the URL of the link.
75 # marks" followed immediately by a colon and the URL of the link.
76 #
76 #
77 # Optional: text in (parentheses) following the link text,
77 # Optional: text in (parentheses) following the link text,
78 # but before the closing quotation mark, will become a Title
78 # but before the closing quotation mark, will become a Title
79 # attribute for the link, visible as a tool tip when a cursor is above it.
79 # attribute for the link, visible as a tool tip when a cursor is above it.
80 #
80 #
81 # Example:
81 # Example:
82 #
82 #
83 # "This is a link (This is a title) ":http://www.textism.com
83 # "This is a link (This is a title) ":http://www.textism.com
84 #
84 #
85 # Will become:
85 # Will become:
86 #
86 #
87 # <a href="http://www.textism.com" title="This is a title">This is a link</a>
87 # <a href="http://www.textism.com" title="This is a title">This is a link</a>
88 #
88 #
89 # == Images
89 # == Images
90 #
90 #
91 # To insert an image, put the URL for the image inside exclamation marks.
91 # To insert an image, put the URL for the image inside exclamation marks.
92 #
92 #
93 # Optional: text that immediately follows the URL in (parentheses) will
93 # Optional: text that immediately follows the URL in (parentheses) will
94 # be used as the Alt text for the image. Images on the web should always
94 # be used as the Alt text for the image. Images on the web should always
95 # have descriptive Alt text for the benefit of readers using non-graphical
95 # have descriptive Alt text for the benefit of readers using non-graphical
96 # browsers.
96 # browsers.
97 #
97 #
98 # Optional: place a colon followed by a URL immediately after the
98 # Optional: place a colon followed by a URL immediately after the
99 # closing ! to make the image into a link.
99 # closing ! to make the image into a link.
100 #
100 #
101 # Example:
101 # Example:
102 #
102 #
103 # !http://www.textism.com/common/textist.gif(Textist)!
103 # !http://www.textism.com/common/textist.gif(Textist)!
104 #
104 #
105 # Will become:
105 # Will become:
106 #
106 #
107 # <img src="http://www.textism.com/common/textist.gif" alt="Textist" />
107 # <img src="http://www.textism.com/common/textist.gif" alt="Textist" />
108 #
108 #
109 # With a link:
109 # With a link:
110 #
110 #
111 # !/common/textist.gif(Textist)!:http://textism.com
111 # !/common/textist.gif(Textist)!:http://textism.com
112 #
112 #
113 # Will become:
113 # Will become:
114 #
114 #
115 # <a href="http://textism.com"><img src="/common/textist.gif" alt="Textist" /></a>
115 # <a href="http://textism.com"><img src="/common/textist.gif" alt="Textist" /></a>
116 #
116 #
117 # == Defining Acronyms
117 # == Defining Acronyms
118 #
118 #
119 # HTML allows authors to define acronyms via the tag. The definition appears as a
119 # HTML allows authors to define acronyms via the tag. The definition appears as a
120 # tool tip when a cursor hovers over the acronym. A crucial aid to clear writing,
120 # tool tip when a cursor hovers over the acronym. A crucial aid to clear writing,
121 # this should be used at least once for each acronym in documents where they appear.
121 # this should be used at least once for each acronym in documents where they appear.
122 #
122 #
123 # To quickly define an acronym in Textile, place the full text in (parentheses)
123 # To quickly define an acronym in Textile, place the full text in (parentheses)
124 # immediately following the acronym.
124 # immediately following the acronym.
125 #
125 #
126 # Example:
126 # Example:
127 #
127 #
128 # ACLU(American Civil Liberties Union)
128 # ACLU(American Civil Liberties Union)
129 #
129 #
130 # Will become:
130 # Will become:
131 #
131 #
132 # <abbr title="American Civil Liberties Union">ACLU</abbr>
132 # <abbr title="American Civil Liberties Union">ACLU</abbr>
133 #
133 #
134 # == Adding Tables
134 # == Adding Tables
135 #
135 #
136 # In Textile, simple tables can be added by separating each column by
136 # In Textile, simple tables can be added by separating each column by
137 # a pipe.
137 # a pipe.
138 #
138 #
139 # |a|simple|table|row|
139 # |a|simple|table|row|
140 # |And|Another|table|row|
140 # |And|Another|table|row|
141 #
141 #
142 # Attributes are defined by style definitions in parentheses.
142 # Attributes are defined by style definitions in parentheses.
143 #
143 #
144 # table(border:1px solid black).
144 # table(border:1px solid black).
145 # (background:#ddd;color:red). |{}| | | |
145 # (background:#ddd;color:red). |{}| | | |
146 #
146 #
147 # == Using RedCloth
147 # == Using RedCloth
148 #
148 #
149 # RedCloth is simply an extension of the String class, which can handle
149 # RedCloth is simply an extension of the String class, which can handle
150 # Textile formatting. Use it like a String and output HTML with its
150 # Textile formatting. Use it like a String and output HTML with its
151 # RedCloth#to_html method.
151 # RedCloth#to_html method.
152 #
152 #
153 # doc = RedCloth.new "
153 # doc = RedCloth.new "
154 #
154 #
155 # h2. Test document
155 # h2. Test document
156 #
156 #
157 # Just a simple test."
157 # Just a simple test."
158 #
158 #
159 # puts doc.to_html
159 # puts doc.to_html
160 #
160 #
161 # By default, RedCloth uses both Textile and Markdown formatting, with
161 # By default, RedCloth uses both Textile and Markdown formatting, with
162 # Textile formatting taking precedence. If you want to turn off Markdown
162 # Textile formatting taking precedence. If you want to turn off Markdown
163 # formatting, to boost speed and limit the processor:
163 # formatting, to boost speed and limit the processor:
164 #
164 #
165 # class RedCloth::Textile.new( str )
165 # class RedCloth::Textile.new( str )
166
166
167 class RedCloth3 < String
167 class RedCloth3 < String
168 include Redmine::Helpers::URL
168 include Redmine::Helpers::URL
169
169
170 VERSION = '3.0.4'
170 VERSION = '3.0.4'
171 DEFAULT_RULES = [:textile, :markdown]
171 DEFAULT_RULES = [:textile, :markdown]
172
172
173 #
173 #
174 # Two accessor for setting security restrictions.
174 # Two accessor for setting security restrictions.
175 #
175 #
176 # This is a nice thing if you're using RedCloth for
176 # This is a nice thing if you're using RedCloth for
177 # formatting in public places (e.g. Wikis) where you
177 # formatting in public places (e.g. Wikis) where you
178 # don't want users to abuse HTML for bad things.
178 # don't want users to abuse HTML for bad things.
179 #
179 #
180 # If +:filter_html+ is set, HTML which wasn't
180 # If +:filter_html+ is set, HTML which wasn't
181 # created by the Textile processor will be escaped.
181 # created by the Textile processor will be escaped.
182 #
182 #
183 # If +:filter_styles+ is set, it will also disable
183 # If +:filter_styles+ is set, it will also disable
184 # the style markup specifier. ('{color: red}')
184 # the style markup specifier. ('{color: red}')
185 #
185 #
186 attr_accessor :filter_html, :filter_styles
186 attr_accessor :filter_html, :filter_styles
187
187
188 #
188 #
189 # Accessor for toggling hard breaks.
189 # Accessor for toggling hard breaks.
190 #
190 #
191 # If +:hard_breaks+ is set, single newlines will
191 # If +:hard_breaks+ is set, single newlines will
192 # be converted to HTML break tags. This is the
192 # be converted to HTML break tags. This is the
193 # default behavior for traditional RedCloth.
193 # default behavior for traditional RedCloth.
194 #
194 #
195 attr_accessor :hard_breaks
195 attr_accessor :hard_breaks
196
196
197 # Accessor for toggling lite mode.
197 # Accessor for toggling lite mode.
198 #
198 #
199 # In lite mode, block-level rules are ignored. This means
199 # In lite mode, block-level rules are ignored. This means
200 # that tables, paragraphs, lists, and such aren't available.
200 # that tables, paragraphs, lists, and such aren't available.
201 # Only the inline markup for bold, italics, entities and so on.
201 # Only the inline markup for bold, italics, entities and so on.
202 #
202 #
203 # r = RedCloth.new( "And then? She *fell*!", [:lite_mode] )
203 # r = RedCloth.new( "And then? She *fell*!", [:lite_mode] )
204 # r.to_html
204 # r.to_html
205 # #=> "And then? She <strong>fell</strong>!"
205 # #=> "And then? She <strong>fell</strong>!"
206 #
206 #
207 attr_accessor :lite_mode
207 attr_accessor :lite_mode
208
208
209 #
209 #
210 # Accessor for toggling span caps.
210 # Accessor for toggling span caps.
211 #
211 #
212 # Textile places `span' tags around capitalized
212 # Textile places `span' tags around capitalized
213 # words by default, but this wreaks havoc on Wikis.
213 # words by default, but this wreaks havoc on Wikis.
214 # If +:no_span_caps+ is set, this will be
214 # If +:no_span_caps+ is set, this will be
215 # suppressed.
215 # suppressed.
216 #
216 #
217 attr_accessor :no_span_caps
217 attr_accessor :no_span_caps
218
218
219 #
219 #
220 # Establishes the markup predence. Available rules include:
220 # Establishes the markup predence. Available rules include:
221 #
221 #
222 # == Textile Rules
222 # == Textile Rules
223 #
223 #
224 # The following textile rules can be set individually. Or add the complete
224 # The following textile rules can be set individually. Or add the complete
225 # set of rules with the single :textile rule, which supplies the rule set in
225 # set of rules with the single :textile rule, which supplies the rule set in
226 # the following precedence:
226 # the following precedence:
227 #
227 #
228 # refs_textile:: Textile references (i.e. [hobix]http://hobix.com/)
228 # refs_textile:: Textile references (i.e. [hobix]http://hobix.com/)
229 # block_textile_table:: Textile table block structures
229 # block_textile_table:: Textile table block structures
230 # block_textile_lists:: Textile list structures
230 # block_textile_lists:: Textile list structures
231 # block_textile_prefix:: Textile blocks with prefixes (i.e. bq., h2., etc.)
231 # block_textile_prefix:: Textile blocks with prefixes (i.e. bq., h2., etc.)
232 # inline_textile_image:: Textile inline images
232 # inline_textile_image:: Textile inline images
233 # inline_textile_link:: Textile inline links
233 # inline_textile_link:: Textile inline links
234 # inline_textile_span:: Textile inline spans
234 # inline_textile_span:: Textile inline spans
235 # glyphs_textile:: Textile entities (such as em-dashes and smart quotes)
235 # glyphs_textile:: Textile entities (such as em-dashes and smart quotes)
236 #
236 #
237 # == Markdown
237 # == Markdown
238 #
238 #
239 # refs_markdown:: Markdown references (for example: [hobix]: http://hobix.com/)
239 # refs_markdown:: Markdown references (for example: [hobix]: http://hobix.com/)
240 # block_markdown_setext:: Markdown setext headers
240 # block_markdown_setext:: Markdown setext headers
241 # block_markdown_atx:: Markdown atx headers
241 # block_markdown_atx:: Markdown atx headers
242 # block_markdown_rule:: Markdown horizontal rules
242 # block_markdown_rule:: Markdown horizontal rules
243 # block_markdown_bq:: Markdown blockquotes
243 # block_markdown_bq:: Markdown blockquotes
244 # block_markdown_lists:: Markdown lists
244 # block_markdown_lists:: Markdown lists
245 # inline_markdown_link:: Markdown links
245 # inline_markdown_link:: Markdown links
246 attr_accessor :rules
246 attr_accessor :rules
247
247
248 # Returns a new RedCloth object, based on _string_ and
248 # Returns a new RedCloth object, based on _string_ and
249 # enforcing all the included _restrictions_.
249 # enforcing all the included _restrictions_.
250 #
250 #
251 # r = RedCloth.new( "h1. A <b>bold</b> man", [:filter_html] )
251 # r = RedCloth.new( "h1. A <b>bold</b> man", [:filter_html] )
252 # r.to_html
252 # r.to_html
253 # #=>"<h1>A &lt;b&gt;bold&lt;/b&gt; man</h1>"
253 # #=>"<h1>A &lt;b&gt;bold&lt;/b&gt; man</h1>"
254 #
254 #
255 def initialize( string, restrictions = [] )
255 def initialize( string, restrictions = [] )
256 restrictions.each { |r| method( "#{ r }=" ).call( true ) }
256 restrictions.each { |r| method( "#{ r }=" ).call( true ) }
257 super( string )
257 super( string )
258 end
258 end
259
259
260 #
260 #
261 # Generates HTML from the Textile contents.
261 # Generates HTML from the Textile contents.
262 #
262 #
263 # r = RedCloth.new( "And then? She *fell*!" )
263 # r = RedCloth.new( "And then? She *fell*!" )
264 # r.to_html( true )
264 # r.to_html( true )
265 # #=>"And then? She <strong>fell</strong>!"
265 # #=>"And then? She <strong>fell</strong>!"
266 #
266 #
267 def to_html( *rules )
267 def to_html( *rules )
268 rules = DEFAULT_RULES if rules.empty?
268 rules = DEFAULT_RULES if rules.empty?
269 # make our working copy
269 # make our working copy
270 text = self.dup
270 text = self.dup
271
271
272 @urlrefs = {}
272 @urlrefs = {}
273 @shelf = []
273 @shelf = []
274 textile_rules = [:block_textile_table, :block_textile_lists,
274 textile_rules = [:block_textile_table, :block_textile_lists,
275 :block_textile_prefix, :inline_textile_image, :inline_textile_link,
275 :block_textile_prefix, :inline_textile_image, :inline_textile_link,
276 :inline_textile_code, :inline_textile_span, :glyphs_textile]
276 :inline_textile_code, :inline_textile_span, :glyphs_textile]
277 markdown_rules = [:refs_markdown, :block_markdown_setext, :block_markdown_atx, :block_markdown_rule,
277 markdown_rules = [:refs_markdown, :block_markdown_setext, :block_markdown_atx, :block_markdown_rule,
278 :block_markdown_bq, :block_markdown_lists,
278 :block_markdown_bq, :block_markdown_lists,
279 :inline_markdown_reflink, :inline_markdown_link]
279 :inline_markdown_reflink, :inline_markdown_link]
280 @rules = rules.collect do |rule|
280 @rules = rules.collect do |rule|
281 case rule
281 case rule
282 when :markdown
282 when :markdown
283 markdown_rules
283 markdown_rules
284 when :textile
284 when :textile
285 textile_rules
285 textile_rules
286 else
286 else
287 rule
287 rule
288 end
288 end
289 end.flatten
289 end.flatten
290
290
291 # standard clean up
291 # standard clean up
292 incoming_entities text
292 incoming_entities text
293 clean_white_space text
293 clean_white_space text
294
294
295 # start processor
295 # start processor
296 @pre_list = []
296 @pre_list = []
297 rip_offtags text
297 rip_offtags text
298 no_textile text
298 no_textile text
299 escape_html_tags text
299 escape_html_tags text
300 # need to do this before #hard_break and #blocks
300 # need to do this before #hard_break and #blocks
301 block_textile_quotes text unless @lite_mode
301 block_textile_quotes text unless @lite_mode
302 hard_break text
302 hard_break text
303 unless @lite_mode
303 unless @lite_mode
304 refs text
304 refs text
305 blocks text
305 blocks text
306 end
306 end
307 inline text
307 inline text
308 smooth_offtags text
308 smooth_offtags text
309
309
310 retrieve text
310 retrieve text
311
311
312 text.gsub!( /<\/?notextile>/, '' )
312 text.gsub!( /<\/?notextile>/, '' )
313 text.gsub!( /x%x%/, '&#38;' )
313 text.gsub!( /x%x%/, '&#38;' )
314 clean_html text if filter_html
314 clean_html text if filter_html
315 text.strip!
315 text.strip!
316 text
316 text
317
317
318 end
318 end
319
319
320 #######
320 #######
321 private
321 private
322 #######
322 #######
323 #
323 #
324 # Mapping of 8-bit ASCII codes to HTML numerical entity equivalents.
324 # Mapping of 8-bit ASCII codes to HTML numerical entity equivalents.
325 # (from PyTextile)
325 # (from PyTextile)
326 #
326 #
327 TEXTILE_TAGS =
327 TEXTILE_TAGS =
328
328
329 [[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230],
329 [[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230],
330 [134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249],
330 [134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249],
331 [140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217],
331 [140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217],
332 [147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732],
332 [147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732],
333 [153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]].
333 [153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]].
334
334
335 collect! do |a, b|
335 collect! do |a, b|
336 [a.chr, ( b.zero? and "" or "&#{ b };" )]
336 [a.chr, ( b.zero? and "" or "&#{ b };" )]
337 end
337 end
338
338
339 #
339 #
340 # Regular expressions to convert to HTML.
340 # Regular expressions to convert to HTML.
341 #
341 #
342 A_HLGN = /(?:(?:<>|<|>|\=|[()]+)+)/
342 A_HLGN = /(?:(?:<>|<|>|\=|[()]+)+)/
343 A_VLGN = /[\-^~]/
343 A_VLGN = /[\-^~]/
344 C_CLAS = '(?:\([^")]+\))'
344 C_CLAS = '(?:\([^")]+\))'
345 C_LNGE = '(?:\[[a-z\-_]+\])'
345 C_LNGE = '(?:\[[a-z\-_]+\])'
346 C_STYL = '(?:\{[^"}]+\})'
346 C_STYL = '(?:\{[^"}]+\})'
347 S_CSPN = '(?:\\\\\d+)'
347 S_CSPN = '(?:\\\\\d+)'
348 S_RSPN = '(?:/\d+)'
348 S_RSPN = '(?:/\d+)'
349 A = "(?:#{A_HLGN}?#{A_VLGN}?|#{A_VLGN}?#{A_HLGN}?)"
349 A = "(?:#{A_HLGN}?#{A_VLGN}?|#{A_VLGN}?#{A_HLGN}?)"
350 S = "(?:#{S_CSPN}?#{S_RSPN}|#{S_RSPN}?#{S_CSPN}?)"
350 S = "(?:#{S_CSPN}?#{S_RSPN}|#{S_RSPN}?#{S_CSPN}?)"
351 C = "(?:#{C_CLAS}?#{C_STYL}?#{C_LNGE}?|#{C_STYL}?#{C_LNGE}?#{C_CLAS}?|#{C_LNGE}?#{C_STYL}?#{C_CLAS}?)"
351 C = "(?:#{C_CLAS}?#{C_STYL}?#{C_LNGE}?|#{C_STYL}?#{C_LNGE}?#{C_CLAS}?|#{C_LNGE}?#{C_STYL}?#{C_CLAS}?)"
352 # PUNCT = Regexp::quote( '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' )
352 # PUNCT = Regexp::quote( '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' )
353 PUNCT = Regexp::quote( '!"#$%&\'*+,-./:;=?@\\^_`|~' )
353 PUNCT = Regexp::quote( '!"#$%&\'*+,-./:;=?@\\^_`|~' )
354 PUNCT_NOQ = Regexp::quote( '!"#$&\',./:;=?@\\`|' )
354 PUNCT_NOQ = Regexp::quote( '!"#$&\',./:;=?@\\`|' )
355 PUNCT_Q = Regexp::quote( '*-_+^~%' )
355 PUNCT_Q = Regexp::quote( '*-_+^~%' )
356 HYPERLINK = '(\S+?)([^\w\s/;=\?]*?)(?=\s|<|$)'
356 HYPERLINK = '(\S+?)([^\w\s/;=\?]*?)(?=\s|<|$)'
357
357
358 # Text markup tags, don't conflict with block tags
358 # Text markup tags, don't conflict with block tags
359 SIMPLE_HTML_TAGS = [
359 SIMPLE_HTML_TAGS = [
360 'tt', 'b', 'i', 'big', 'small', 'em', 'strong', 'dfn', 'code',
360 'tt', 'b', 'i', 'big', 'small', 'em', 'strong', 'dfn', 'code',
361 'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'a', 'img', 'br',
361 'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'a', 'img', 'br',
362 'br', 'map', 'q', 'sub', 'sup', 'span', 'bdo'
362 'br', 'map', 'q', 'sub', 'sup', 'span', 'bdo'
363 ]
363 ]
364
364
365 QTAGS = [
365 QTAGS = [
366 ['**', 'b', :limit],
366 ['**', 'b', :limit],
367 ['*', 'strong', :limit],
367 ['*', 'strong', :limit],
368 ['??', 'cite', :limit],
368 ['??', 'cite', :limit],
369 ['-', 'del', :limit],
369 ['-', 'del', :limit],
370 ['__', 'i', :limit],
370 ['__', 'i', :limit],
371 ['_', 'em', :limit],
371 ['_', 'em', :limit],
372 ['%', 'span', :limit],
372 ['%', 'span', :limit],
373 ['+', 'ins', :limit],
373 ['+', 'ins', :limit],
374 ['^', 'sup', :limit],
374 ['^', 'sup', :limit],
375 ['~', 'sub', :limit]
375 ['~', 'sub', :limit]
376 ]
376 ]
377 QTAGS_JOIN = QTAGS.map {|rc, ht, rtype| Regexp::quote rc}.join('|')
377 QTAGS_JOIN = QTAGS.map {|rc, ht, rtype| Regexp::quote rc}.join('|')
378
378
379 QTAGS.collect! do |rc, ht, rtype|
379 QTAGS.collect! do |rc, ht, rtype|
380 rcq = Regexp::quote rc
380 rcq = Regexp::quote rc
381 re =
381 re =
382 case rtype
382 case rtype
383 when :limit
383 when :limit
384 /(^|[>\s\(]) # sta
384 /(^|[>\s\(]) # sta
385 (?!\-\-)
385 (?!\-\-)
386 (#{QTAGS_JOIN}|) # oqs
386 (#{QTAGS_JOIN}|) # oqs
387 (#{rcq}) # qtag
387 (#{rcq}) # qtag
388 ([[:word:]]|[^\s].*?[^\s]) # content
388 ([[:word:]]|[^\s].*?[^\s]) # content
389 (?!\-\-)
389 (?!\-\-)
390 #{rcq}
390 #{rcq}
391 (#{QTAGS_JOIN}|) # oqa
391 (#{QTAGS_JOIN}|) # oqa
392 (?=[[:punct:]]|<|\s|\)|$)/x
392 (?=[[:punct:]]|<|\s|\)|$)/x
393 else
393 else
394 /(#{rcq})
394 /(#{rcq})
395 (#{C})
395 (#{C})
396 (?::(\S+))?
396 (?::(\S+))?
397 ([[:word:]]|[^\s\-].*?[^\s\-])
397 ([[:word:]]|[^\s\-].*?[^\s\-])
398 #{rcq}/xm
398 #{rcq}/xm
399 end
399 end
400 [rc, ht, re, rtype]
400 [rc, ht, re, rtype]
401 end
401 end
402
402
403 # Elements to handle
403 # Elements to handle
404 GLYPHS = [
404 GLYPHS = [
405 # [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1&#8217;\2' ], # single closing
405 # [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1&#8217;\2' ], # single closing
406 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)\'/, '\1&#8217;' ], # single closing
406 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)\'/, '\1&#8217;' ], # single closing
407 # [ /\'(?=[#{PUNCT_Q}]*(s\b|[\s#{PUNCT_NOQ}]))/, '&#8217;' ], # single closing
407 # [ /\'(?=[#{PUNCT_Q}]*(s\b|[\s#{PUNCT_NOQ}]))/, '&#8217;' ], # single closing
408 # [ /\'/, '&#8216;' ], # single opening
408 # [ /\'/, '&#8216;' ], # single opening
409 # [ /</, '&lt;' ], # less-than
409 # [ /</, '&lt;' ], # less-than
410 # [ />/, '&gt;' ], # greater-than
410 # [ />/, '&gt;' ], # greater-than
411 # [ /([^\s\[{(])?"(\s|:|$)/, '\1&#8221;\2' ], # double closing
411 # [ /([^\s\[{(])?"(\s|:|$)/, '\1&#8221;\2' ], # double closing
412 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)"/, '\1&#8221;' ], # double closing
412 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)"/, '\1&#8221;' ], # double closing
413 # [ /"(?=[#{PUNCT_Q}]*[\s#{PUNCT_NOQ}])/, '&#8221;' ], # double closing
413 # [ /"(?=[#{PUNCT_Q}]*[\s#{PUNCT_NOQ}])/, '&#8221;' ], # double closing
414 # [ /"/, '&#8220;' ], # double opening
414 # [ /"/, '&#8220;' ], # double opening
415 # [ /\b( )?\.{3}/, '\1&#8230;' ], # ellipsis
415 # [ /\b( )?\.{3}/, '\1&#8230;' ], # ellipsis
416 # [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '<acronym title="\2">\1</acronym>' ], # 3+ uppercase acronym
416 # [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '<acronym title="\2">\1</acronym>' ], # 3+ uppercase acronym
417 # [ /(^|[^"][>\s])([A-Z][A-Z0-9 ]+[A-Z0-9])([^<A-Za-z0-9]|$)/, '\1<span class="caps">\2</span>\3', :no_span_caps ], # 3+ uppercase caps
417 # [ /(^|[^"][>\s])([A-Z][A-Z0-9 ]+[A-Z0-9])([^<A-Za-z0-9]|$)/, '\1<span class="caps">\2</span>\3', :no_span_caps ], # 3+ uppercase caps
418 # [ /(\.\s)?\s?--\s?/, '\1&#8212;' ], # em dash
418 # [ /(\.\s)?\s?--\s?/, '\1&#8212;' ], # em dash
419 # [ /\s->\s/, ' &rarr; ' ], # right arrow
419 # [ /\s->\s/, ' &rarr; ' ], # right arrow
420 # [ /\s-\s/, ' &#8211; ' ], # en dash
420 # [ /\s-\s/, ' &#8211; ' ], # en dash
421 # [ /(\d+) ?x ?(\d+)/, '\1&#215;\2' ], # dimension sign
421 # [ /(\d+) ?x ?(\d+)/, '\1&#215;\2' ], # dimension sign
422 # [ /\b ?[(\[]TM[\])]/i, '&#8482;' ], # trademark
422 # [ /\b ?[(\[]TM[\])]/i, '&#8482;' ], # trademark
423 # [ /\b ?[(\[]R[\])]/i, '&#174;' ], # registered
423 # [ /\b ?[(\[]R[\])]/i, '&#174;' ], # registered
424 # [ /\b ?[(\[]C[\])]/i, '&#169;' ] # copyright
424 # [ /\b ?[(\[]C[\])]/i, '&#169;' ] # copyright
425 ]
425 ]
426
426
427 H_ALGN_VALS = {
427 H_ALGN_VALS = {
428 '<' => 'left',
428 '<' => 'left',
429 '=' => 'center',
429 '=' => 'center',
430 '>' => 'right',
430 '>' => 'right',
431 '<>' => 'justify'
431 '<>' => 'justify'
432 }
432 }
433
433
434 V_ALGN_VALS = {
434 V_ALGN_VALS = {
435 '^' => 'top',
435 '^' => 'top',
436 '-' => 'middle',
436 '-' => 'middle',
437 '~' => 'bottom'
437 '~' => 'bottom'
438 }
438 }
439
439
440 #
440 #
441 # Flexible HTML escaping
441 # Flexible HTML escaping
442 #
442 #
443 def htmlesc( str, mode=:Quotes )
443 def htmlesc( str, mode=:Quotes )
444 if str
444 if str
445 str.gsub!( '&', '&amp;' )
445 str.gsub!( '&', '&amp;' )
446 str.gsub!( '"', '&quot;' ) if mode != :NoQuotes
446 str.gsub!( '"', '&quot;' ) if mode != :NoQuotes
447 str.gsub!( "'", '&#039;' ) if mode == :Quotes
447 str.gsub!( "'", '&#039;' ) if mode == :Quotes
448 str.gsub!( '<', '&lt;')
448 str.gsub!( '<', '&lt;')
449 str.gsub!( '>', '&gt;')
449 str.gsub!( '>', '&gt;')
450 end
450 end
451 str
451 str
452 end
452 end
453
453
454 # Search and replace for Textile glyphs (quotes, dashes, other symbols)
454 # Search and replace for Textile glyphs (quotes, dashes, other symbols)
455 def pgl( text )
455 def pgl( text )
456 #GLYPHS.each do |re, resub, tog|
456 #GLYPHS.each do |re, resub, tog|
457 # next if tog and method( tog ).call
457 # next if tog and method( tog ).call
458 # text.gsub! re, resub
458 # text.gsub! re, resub
459 #end
459 #end
460 text.gsub!(/\b([A-Z][A-Z0-9]{1,})\b(?:[(]([^)]*)[)])/) do |m|
460 text.gsub!(/\b([A-Z][A-Z0-9]{1,})\b(?:[(]([^)]*)[)])/) do |m|
461 "<abbr title=\"#{htmlesc $2}\">#{$1}</abbr>"
461 "<abbr title=\"#{htmlesc $2}\">#{$1}</abbr>"
462 end
462 end
463 end
463 end
464
464
465 # Parses Textile attribute lists and builds an HTML attribute string
465 # Parses Textile attribute lists and builds an HTML attribute string
466 def pba( text_in, element = "" )
466 def pba( text_in, element = "" )
467
467
468 return '' unless text_in
468 return '' unless text_in
469
469
470 style = []
470 style = []
471 text = text_in.dup
471 text = text_in.dup
472 if element == 'td'
472 if element == 'td'
473 colspan = $1 if text =~ /\\(\d+)/
473 colspan = $1 if text =~ /\\(\d+)/
474 rowspan = $1 if text =~ /\/(\d+)/
474 rowspan = $1 if text =~ /\/(\d+)/
475 style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN
475 style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN
476 end
476 end
477
477
478 if text.sub!( /\{([^"}]*)\}/, '' ) && !filter_styles
478 if text.sub!( /\{([^"}]*)\}/, '' ) && !filter_styles
479 sanitized = sanitize_styles($1)
479 sanitized = sanitize_styles($1)
480 style << "#{ sanitized };" unless sanitized.blank?
480 style << "#{ sanitized };" unless sanitized.blank?
481 end
481 end
482
482
483 lang = $1 if
483 lang = $1 if
484 text.sub!( /\[([a-z\-_]+?)\]/, '' )
484 text.sub!( /\[([a-z\-_]+?)\]/, '' )
485
485
486 cls = $1 if
486 cls = $1 if
487 text.sub!( /\(([^()]+?)\)/, '' )
487 text.sub!( /\(([^()]+?)\)/, '' )
488
488
489 style << "padding-left:#{ $1.length }em;" if
489 style << "padding-left:#{ $1.length }em;" if
490 text.sub!( /([(]+)/, '' )
490 text.sub!( /([(]+)/, '' )
491
491
492 style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' )
492 style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' )
493
493
494 style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN
494 style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN
495
495
496 cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/
496 cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/
497
497
498 atts = ''
498 atts = ''
499 atts << " style=\"#{ style.join }\"" unless style.empty?
499 atts << " style=\"#{ style.join }\"" unless style.empty?
500 atts << " class=\"#{ cls }\"" unless cls.to_s.empty?
500 atts << " class=\"#{ cls }\"" unless cls.to_s.empty?
501 atts << " lang=\"#{ lang }\"" if lang
501 atts << " lang=\"#{ lang }\"" if lang
502 atts << " id=\"#{ id }\"" if id
502 atts << " id=\"#{ id }\"" if id
503 atts << " colspan=\"#{ colspan }\"" if colspan
503 atts << " colspan=\"#{ colspan }\"" if colspan
504 atts << " rowspan=\"#{ rowspan }\"" if rowspan
504 atts << " rowspan=\"#{ rowspan }\"" if rowspan
505
505
506 atts
506 atts
507 end
507 end
508
508
509 STYLES_RE = /^(color|width|height|border|background|padding|margin|font|text|float)(-[a-z]+)*:\s*((\d+%?|\d+px|\d+(\.\d+)?em|#[0-9a-f]+|[a-z]+)\s*)+$/i
509 STYLES_RE = /^(color|width|height|border|background|padding|margin|font|text|float)(-[a-z]+)*:\s*((\d+%?|\d+px|\d+(\.\d+)?em|#[0-9a-f]+|[a-z]+)\s*)+$/i
510
510
511 def sanitize_styles(str)
511 def sanitize_styles(str)
512 styles = str.split(";").map(&:strip)
512 styles = str.split(";").map(&:strip)
513 styles.reject! do |style|
513 styles.reject! do |style|
514 !style.match(STYLES_RE)
514 !style.match(STYLES_RE)
515 end
515 end
516 styles.join(";")
516 styles.join(";")
517 end
517 end
518
518
519 TABLE_RE = /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)(\n\n|\Z)/m
519 TABLE_RE = /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)(\n\n|\Z)/m
520
520
521 # Parses a Textile table block, building HTML from the result.
521 # Parses a Textile table block, building HTML from the result.
522 def block_textile_table( text )
522 def block_textile_table( text )
523 text.gsub!( TABLE_RE ) do |matches|
523 text.gsub!( TABLE_RE ) do |matches|
524
524
525 tatts, fullrow = $~[1..2]
525 tatts, fullrow = $~[1..2]
526 tatts = pba( tatts, 'table' )
526 tatts = pba( tatts, 'table' )
527 tatts = shelve( tatts ) if tatts
527 tatts = shelve( tatts ) if tatts
528 rows = []
528 rows = []
529 fullrow.gsub!(/([^|\s])\s*\n/, "\\1<br />")
529 fullrow.gsub!(/([^|\s])\s*\n/, "\\1<br />")
530 fullrow.each_line do |row|
530 fullrow.each_line do |row|
531 ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m
531 ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m
532 cells = []
532 cells = []
533 # the regexp prevents wiki links with a | from being cut as cells
533 # the regexp prevents wiki links with a | from being cut as cells
534 row.scan(/\|(_?#{S}#{A}#{C}\. ?)?((\[\[[^|\]]*\|[^|\]]*\]\]|[^|])*?)(?=\|)/) do |modifiers, cell|
534 row.scan(/\|(_?#{S}#{A}#{C}\. ?)?((\[\[[^|\]]*\|[^|\]]*\]\]|[^|])*?)(?=\|)/) do |modifiers, cell|
535 ctyp = 'd'
535 ctyp = 'd'
536 ctyp = 'h' if modifiers && modifiers =~ /^_/
536 ctyp = 'h' if modifiers && modifiers =~ /^_/
537
537
538 catts = nil
538 catts = nil
539 catts = pba( modifiers, 'td' ) if modifiers
539 catts = pba( modifiers, 'td' ) if modifiers
540
540
541 catts = shelve( catts ) if catts
541 catts = shelve( catts ) if catts
542 cells << "\t\t\t<t#{ ctyp }#{ catts }>#{ cell }</t#{ ctyp }>"
542 cells << "\t\t\t<t#{ ctyp }#{ catts }>#{ cell }</t#{ ctyp }>"
543 end
543 end
544 ratts = shelve( ratts ) if ratts
544 ratts = shelve( ratts ) if ratts
545 rows << "\t\t<tr#{ ratts }>\n#{ cells.join( "\n" ) }\n\t\t</tr>"
545 rows << "\t\t<tr#{ ratts }>\n#{ cells.join( "\n" ) }\n\t\t</tr>"
546 end
546 end
547 "\t<table#{ tatts }>\n#{ rows.join( "\n" ) }\n\t</table>\n\n"
547 "\t<table#{ tatts }>\n#{ rows.join( "\n" ) }\n\t</table>\n\n"
548 end
548 end
549 end
549 end
550
550
551 LISTS_RE = /^([#*]+?#{C} .*?)$(?![^#*])/m
551 LISTS_RE = /^([#*]+?#{C} .*?)$(?![^#*])/m
552 LISTS_CONTENT_RE = /^([#*]+)(#{A}#{C}) (.*)$/m
552 LISTS_CONTENT_RE = /^([#*]+)(#{A}#{C}) (.*)$/m
553
553
554 # Parses Textile lists and generates HTML
554 # Parses Textile lists and generates HTML
555 def block_textile_lists( text )
555 def block_textile_lists( text )
556 text.gsub!( LISTS_RE ) do |match|
556 text.gsub!( LISTS_RE ) do |match|
557 lines = match.split( /\n/ )
557 lines = match.split( /\n/ )
558 last_line = -1
558 last_line = -1
559 depth = []
559 depth = []
560 lines.each_with_index do |line, line_id|
560 lines.each_with_index do |line, line_id|
561 if line =~ LISTS_CONTENT_RE
561 if line =~ LISTS_CONTENT_RE
562 tl,atts,content = $~[1..3]
562 tl,atts,content = $~[1..3]
563 if depth.last
563 if depth.last
564 if depth.last.length > tl.length
564 if depth.last.length > tl.length
565 (depth.length - 1).downto(0) do |i|
565 (depth.length - 1).downto(0) do |i|
566 break if depth[i].length == tl.length
566 break if depth[i].length == tl.length
567 lines[line_id - 1] << "</li>\n\t</#{ lT( depth[i] ) }l>\n\t"
567 lines[line_id - 1] << "</li>\n\t</#{ lT( depth[i] ) }l>\n\t"
568 depth.pop
568 depth.pop
569 end
569 end
570 end
570 end
571 if depth.last and depth.last.length == tl.length
571 if depth.last and depth.last.length == tl.length
572 lines[line_id - 1] << '</li>'
572 lines[line_id - 1] << '</li>'
573 end
573 end
574 end
574 end
575 unless depth.last == tl
575 unless depth.last == tl
576 depth << tl
576 depth << tl
577 atts = pba( atts )
577 atts = pba( atts )
578 atts = shelve( atts ) if atts
578 atts = shelve( atts ) if atts
579 lines[line_id] = "\t<#{ lT(tl) }l#{ atts }>\n\t<li>#{ content }"
579 lines[line_id] = "\t<#{ lT(tl) }l#{ atts }>\n\t<li>#{ content }"
580 else
580 else
581 lines[line_id] = "\t\t<li>#{ content }"
581 lines[line_id] = "\t\t<li>#{ content }"
582 end
582 end
583 last_line = line_id
583 last_line = line_id
584
584
585 else
585 else
586 last_line = line_id
586 last_line = line_id
587 end
587 end
588 if line_id - last_line > 1 or line_id == lines.length - 1
588 if line_id - last_line > 1 or line_id == lines.length - 1
589 while v = depth.pop
589 while v = depth.pop
590 lines[last_line] << "</li>\n\t</#{ lT( v ) }l>"
590 lines[last_line] << "</li>\n\t</#{ lT( v ) }l>"
591 end
591 end
592 end
592 end
593 end
593 end
594 lines.join( "\n" )
594 lines.join( "\n" )
595 end
595 end
596 end
596 end
597
597
598 QUOTES_RE = /(^>+([^\n]*?)(\n|$))+/m
598 QUOTES_RE = /(^>+([^\n]*?)(\n|$))+/m
599 QUOTES_CONTENT_RE = /^([> ]+)(.*)$/m
599 QUOTES_CONTENT_RE = /^([> ]+)(.*)$/m
600
600
601 def block_textile_quotes( text )
601 def block_textile_quotes( text )
602 text.gsub!( QUOTES_RE ) do |match|
602 text.gsub!( QUOTES_RE ) do |match|
603 lines = match.split( /\n/ )
603 lines = match.split( /\n/ )
604 quotes = ''
604 quotes = ''
605 indent = 0
605 indent = 0
606 lines.each do |line|
606 lines.each do |line|
607 line =~ QUOTES_CONTENT_RE
607 line =~ QUOTES_CONTENT_RE
608 bq,content = $1, $2
608 bq,content = $1, $2
609 l = bq.count('>')
609 l = bq.count('>')
610 if l != indent
610 if l != indent
611 quotes << ("\n\n" + (l>indent ? '<blockquote>' * (l-indent) : '</blockquote>' * (indent-l)) + "\n\n")
611 quotes << ("\n\n" + (l>indent ? '<blockquote>' * (l-indent) : '</blockquote>' * (indent-l)) + "\n\n")
612 indent = l
612 indent = l
613 end
613 end
614 quotes << (content + "\n")
614 quotes << (content + "\n")
615 end
615 end
616 quotes << ("\n" + '</blockquote>' * indent + "\n\n")
616 quotes << ("\n" + '</blockquote>' * indent + "\n\n")
617 quotes
617 quotes
618 end
618 end
619 end
619 end
620
620
621 CODE_RE = /(\W)
621 CODE_RE = /(\W)
622 @
622 @
623 (?:\|(\w+?)\|)?
623 (?:\|(\w+?)\|)?
624 (.+?)
624 (.+?)
625 @
625 @
626 (?=\W)/x
626 (?=\W)/x
627
627
628 def inline_textile_code( text )
628 def inline_textile_code( text )
629 text.gsub!( CODE_RE ) do |m|
629 text.gsub!( CODE_RE ) do |m|
630 before,lang,code,after = $~[1..4]
630 before,lang,code,after = $~[1..4]
631 lang = " lang=\"#{ lang }\"" if lang
631 lang = " lang=\"#{ lang }\"" if lang
632 rip_offtags( "#{ before }<code#{ lang }>#{ code }</code>#{ after }", false )
632 rip_offtags( "#{ before }<code#{ lang }>#{ code }</code>#{ after }", false )
633 end
633 end
634 end
634 end
635
635
636 def lT( text )
636 def lT( text )
637 text =~ /\#$/ ? 'o' : 'u'
637 text =~ /\#$/ ? 'o' : 'u'
638 end
638 end
639
639
640 def hard_break( text )
640 def hard_break( text )
641 text.gsub!( /(.)\n(?!\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
641 text.gsub!( /(.)\n(?!\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
642 end
642 end
643
643
644 BLOCKS_GROUP_RE = /\n{2,}(?! )/m
644 BLOCKS_GROUP_RE = /\n{2,}(?! )/m
645
645
646 def blocks( text, deep_code = false )
646 def blocks( text, deep_code = false )
647 text.replace( text.split( BLOCKS_GROUP_RE ).collect do |blk|
647 text.replace( text.split( BLOCKS_GROUP_RE ).collect do |blk|
648 plain = blk !~ /\A[#*> ]/
648 plain = blk !~ /\A[#*> ]/
649
649
650 # skip blocks that are complex HTML
650 # skip blocks that are complex HTML
651 if blk =~ /^<\/?(\w+).*>/ and not SIMPLE_HTML_TAGS.include? $1
651 if blk =~ /^<\/?(\w+).*>/ and not SIMPLE_HTML_TAGS.include? $1
652 blk
652 blk
653 else
653 else
654 # search for indentation levels
654 # search for indentation levels
655 blk.strip!
655 blk.strip!
656 if blk.empty?
656 if blk.empty?
657 blk
657 blk
658 else
658 else
659 code_blk = nil
659 code_blk = nil
660 blk.gsub!( /((?:\n(?:\n^ +[^\n]*)+)+)/m ) do |iblk|
660 blk.gsub!( /((?:\n(?:\n^ +[^\n]*)+)+)/m ) do |iblk|
661 flush_left iblk
661 flush_left iblk
662 blocks iblk, plain
662 blocks iblk, plain
663 iblk.gsub( /^(\S)/, "\t\\1" )
663 iblk.gsub( /^(\S)/, "\t\\1" )
664 if plain
664 if plain
665 code_blk = iblk; ""
665 code_blk = iblk; ""
666 else
666 else
667 iblk
667 iblk
668 end
668 end
669 end
669 end
670
670
671 block_applied = 0
671 block_applied = 0
672 @rules.each do |rule_name|
672 @rules.each do |rule_name|
673 block_applied += 1 if ( rule_name.to_s.match /^block_/ and method( rule_name ).call( blk ) )
673 block_applied += 1 if ( rule_name.to_s.match /^block_/ and method( rule_name ).call( blk ) )
674 end
674 end
675 if block_applied.zero?
675 if block_applied.zero?
676 if deep_code
676 if deep_code
677 blk = "\t<pre><code>#{ blk }</code></pre>"
677 blk = "\t<pre><code>#{ blk }</code></pre>"
678 else
678 else
679 blk = "\t<p>#{ blk }</p>"
679 blk = "\t<p>#{ blk }</p>"
680 end
680 end
681 end
681 end
682 # hard_break blk
682 # hard_break blk
683 blk + "\n#{ code_blk }"
683 blk + "\n#{ code_blk }"
684 end
684 end
685 end
685 end
686
686
687 end.join( "\n\n" ) )
687 end.join( "\n\n" ) )
688 end
688 end
689
689
690 def textile_bq( tag, atts, cite, content )
690 def textile_bq( tag, atts, cite, content )
691 cite, cite_title = check_refs( cite )
691 cite, cite_title = check_refs( cite )
692 cite = " cite=\"#{ cite }\"" if cite
692 cite = " cite=\"#{ cite }\"" if cite
693 atts = shelve( atts ) if atts
693 atts = shelve( atts ) if atts
694 "\t<blockquote#{ cite }>\n\t\t<p#{ atts }>#{ content }</p>\n\t</blockquote>"
694 "\t<blockquote#{ cite }>\n\t\t<p#{ atts }>#{ content }</p>\n\t</blockquote>"
695 end
695 end
696
696
697 def textile_p( tag, atts, cite, content )
697 def textile_p( tag, atts, cite, content )
698 atts = shelve( atts ) if atts
698 atts = shelve( atts ) if atts
699 "\t<#{ tag }#{ atts }>#{ content }</#{ tag }>"
699 "\t<#{ tag }#{ atts }>#{ content }</#{ tag }>"
700 end
700 end
701
701
702 alias textile_h1 textile_p
702 alias textile_h1 textile_p
703 alias textile_h2 textile_p
703 alias textile_h2 textile_p
704 alias textile_h3 textile_p
704 alias textile_h3 textile_p
705 alias textile_h4 textile_p
705 alias textile_h4 textile_p
706 alias textile_h5 textile_p
706 alias textile_h5 textile_p
707 alias textile_h6 textile_p
707 alias textile_h6 textile_p
708
708
709 def textile_fn_( tag, num, atts, cite, content )
709 def textile_fn_( tag, num, atts, cite, content )
710 atts << " id=\"fn#{ num }\" class=\"footnote\""
710 atts << " id=\"fn#{ num }\" class=\"footnote\""
711 content = "<sup>#{ num }</sup> #{ content }"
711 content = "<sup>#{ num }</sup> #{ content }"
712 atts = shelve( atts ) if atts
712 atts = shelve( atts ) if atts
713 "\t<p#{ atts }>#{ content }</p>"
713 "\t<p#{ atts }>#{ content }</p>"
714 end
714 end
715
715
716 BLOCK_RE = /^(([a-z]+)(\d*))(#{A}#{C})\.(?::(\S+))? (.*)$/m
716 BLOCK_RE = /^(([a-z]+)(\d*))(#{A}#{C})\.(?::(\S+))? (.*)$/m
717
717
718 def block_textile_prefix( text )
718 def block_textile_prefix( text )
719 if text =~ BLOCK_RE
719 if text =~ BLOCK_RE
720 tag,tagpre,num,atts,cite,content = $~[1..6]
720 tag,tagpre,num,atts,cite,content = $~[1..6]
721 atts = pba( atts )
721 atts = pba( atts )
722
722
723 # pass to prefix handler
723 # pass to prefix handler
724 replacement = nil
724 replacement = nil
725 if respond_to? "textile_#{ tag }", true
725 if respond_to? "textile_#{ tag }", true
726 replacement = method( "textile_#{ tag }" ).call( tag, atts, cite, content )
726 replacement = method( "textile_#{ tag }" ).call( tag, atts, cite, content )
727 elsif respond_to? "textile_#{ tagpre }_", true
727 elsif respond_to? "textile_#{ tagpre }_", true
728 replacement = method( "textile_#{ tagpre }_" ).call( tagpre, num, atts, cite, content )
728 replacement = method( "textile_#{ tagpre }_" ).call( tagpre, num, atts, cite, content )
729 end
729 end
730 text.gsub!( $& ) { replacement } if replacement
730 text.gsub!( $& ) { replacement } if replacement
731 end
731 end
732 end
732 end
733
733
734 SETEXT_RE = /\A(.+?)\n([=-])[=-]* *$/m
734 SETEXT_RE = /\A(.+?)\n([=-])[=-]* *$/m
735 def block_markdown_setext( text )
735 def block_markdown_setext( text )
736 if text =~ SETEXT_RE
736 if text =~ SETEXT_RE
737 tag = if $2 == "="; "h1"; else; "h2"; end
737 tag = if $2 == "="; "h1"; else; "h2"; end
738 blk, cont = "<#{ tag }>#{ $1 }</#{ tag }>", $'
738 blk, cont = "<#{ tag }>#{ $1 }</#{ tag }>", $'
739 blocks cont
739 blocks cont
740 text.replace( blk + cont )
740 text.replace( blk + cont )
741 end
741 end
742 end
742 end
743
743
744 ATX_RE = /\A(\#{1,6}) # $1 = string of #'s
744 ATX_RE = /\A(\#{1,6}) # $1 = string of #'s
745 [ ]*
745 [ ]*
746 (.+?) # $2 = Header text
746 (.+?) # $2 = Header text
747 [ ]*
747 [ ]*
748 \#* # optional closing #'s (not counted)
748 \#* # optional closing #'s (not counted)
749 $/x
749 $/x
750 def block_markdown_atx( text )
750 def block_markdown_atx( text )
751 if text =~ ATX_RE
751 if text =~ ATX_RE
752 tag = "h#{ $1.length }"
752 tag = "h#{ $1.length }"
753 blk, cont = "<#{ tag }>#{ $2 }</#{ tag }>\n\n", $'
753 blk, cont = "<#{ tag }>#{ $2 }</#{ tag }>\n\n", $'
754 blocks cont
754 blocks cont
755 text.replace( blk + cont )
755 text.replace( blk + cont )
756 end
756 end
757 end
757 end
758
758
759 MARKDOWN_BQ_RE = /\A(^ *> ?.+$(.+\n)*\n*)+/m
759 MARKDOWN_BQ_RE = /\A(^ *> ?.+$(.+\n)*\n*)+/m
760
760
761 def block_markdown_bq( text )
761 def block_markdown_bq( text )
762 text.gsub!( MARKDOWN_BQ_RE ) do |blk|
762 text.gsub!( MARKDOWN_BQ_RE ) do |blk|
763 blk.gsub!( /^ *> ?/, '' )
763 blk.gsub!( /^ *> ?/, '' )
764 flush_left blk
764 flush_left blk
765 blocks blk
765 blocks blk
766 blk.gsub!( /^(\S)/, "\t\\1" )
766 blk.gsub!( /^(\S)/, "\t\\1" )
767 "<blockquote>\n#{ blk }\n</blockquote>\n\n"
767 "<blockquote>\n#{ blk }\n</blockquote>\n\n"
768 end
768 end
769 end
769 end
770
770
771 MARKDOWN_RULE_RE = /^(#{
771 MARKDOWN_RULE_RE = /^(#{
772 ['*', '-', '_'].collect { |ch| ' ?(' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' )
772 ['*', '-', '_'].collect { |ch| ' ?(' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' )
773 })$/
773 })$/
774
774
775 def block_markdown_rule( text )
775 def block_markdown_rule( text )
776 text.gsub!( MARKDOWN_RULE_RE ) do |blk|
776 text.gsub!( MARKDOWN_RULE_RE ) do |blk|
777 "<hr />"
777 "<hr />"
778 end
778 end
779 end
779 end
780
780
781 # XXX TODO XXX
781 # XXX TODO XXX
782 def block_markdown_lists( text )
782 def block_markdown_lists( text )
783 end
783 end
784
784
785 def inline_textile_span( text )
785 def inline_textile_span( text )
786 QTAGS.each do |qtag_rc, ht, qtag_re, rtype|
786 QTAGS.each do |qtag_rc, ht, qtag_re, rtype|
787 text.gsub!( qtag_re ) do |m|
787 text.gsub!( qtag_re ) do |m|
788
788
789 case rtype
789 case rtype
790 when :limit
790 when :limit
791 sta,oqs,qtag,content,oqa = $~[1..6]
791 sta,oqs,qtag,content,oqa = $~[1..6]
792 atts = nil
792 atts = nil
793 if content =~ /^(#{C})(.+)$/
793 if content =~ /^(#{C})(.+)$/
794 atts, content = $~[1..2]
794 atts, content = $~[1..2]
795 end
795 end
796 else
796 else
797 qtag,atts,cite,content = $~[1..4]
797 qtag,atts,cite,content = $~[1..4]
798 sta = ''
798 sta = ''
799 end
799 end
800 atts = pba( atts )
800 atts = pba( atts )
801 atts = shelve( atts ) if atts
801 atts = shelve( atts ) if atts
802
802
803 "#{ sta }#{ oqs }<#{ ht }#{ atts }>#{ content }</#{ ht }>#{ oqa }"
803 "#{ sta }#{ oqs }<#{ ht }#{ atts }>#{ content }</#{ ht }>#{ oqa }"
804
804
805 end
805 end
806 end
806 end
807 end
807 end
808
808
809 LINK_RE = /
809 LINK_RE = /
810 (
810 (
811 ([\s\[{(]|[#{PUNCT}])? # $pre
811 ([\s\[{(]|[#{PUNCT}])? # $pre
812 " # start
812 " # start
813 (#{C}) # $atts
813 (#{C}) # $atts
814 ([^"\n]+?) # $text
814 ([^"\n]+?) # $text
815 \s?
815 \s?
816 (?:\(([^)]+?)\)(?="))? # $title
816 (?:\(([^)]+?)\)(?="))? # $title
817 ":
817 ":
818 ( # $url
818 ( # $url
819 (\/|[a-zA-Z]+:\/\/|www\.|mailto:) # $proto
819 (\/|[a-zA-Z]+:\/\/|www\.|mailto:) # $proto
820 [[:alnum:]_\/]\S+?
820 [[:alnum:]_\/]\S+?
821 )
821 )
822 (\/)? # $slash
822 (\/)? # $slash
823 ([^[:alnum:]_\=\/;\(\)]*?) # $post
823 ([^[:alnum:]_\=\/;\(\)]*?) # $post
824 )
824 )
825 (?=<|\s|$)
825 (?=<|\s|$)
826 /x
826 /x
827 #"
827 #"
828 def inline_textile_link( text )
828 def inline_textile_link( text )
829 text.gsub!( LINK_RE ) do |m|
829 text.gsub!( LINK_RE ) do |m|
830 all,pre,atts,text,title,url,proto,slash,post = $~[1..9]
830 all,pre,atts,text,title,url,proto,slash,post = $~[1..9]
831 if text.include?('<br />')
831 if text.include?('<br />')
832 all
832 all
833 else
833 else
834 url, url_title = check_refs( url )
834 url, url_title = check_refs( url )
835 title ||= url_title
835 title ||= url_title
836
836
837 # Idea below : an URL with unbalanced parethesis and
837 # Idea below : an URL with unbalanced parethesis and
838 # ending by ')' is put into external parenthesis
838 # ending by ')' is put into external parenthesis
839 if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
839 if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
840 url=url[0..-2] # discard closing parenth from url
840 url=url[0..-2] # discard closing parenth from url
841 post = ")"+post # add closing parenth to post
841 post = ")"+post # add closing parenth to post
842 end
842 end
843 atts = pba( atts )
843 atts = pba( atts )
844 atts = " href=\"#{ htmlesc url }#{ slash }\"#{ atts }"
844 atts = " href=\"#{ htmlesc url }#{ slash }\"#{ atts }"
845 atts << " title=\"#{ htmlesc title }\"" if title
845 atts << " title=\"#{ htmlesc title }\"" if title
846 atts = shelve( atts ) if atts
846 atts = shelve( atts ) if atts
847
847
848 external = (url =~ /^https?:\/\//) ? ' class="external"' : ''
848 external = (url =~ /^https?:\/\//) ? ' class="external"' : ''
849
849
850 "#{ pre }<a#{ atts }#{ external }>#{ text }</a>#{ post }"
850 "#{ pre }<a#{ atts }#{ external }>#{ text }</a>#{ post }"
851 end
851 end
852 end
852 end
853 end
853 end
854
854
855 MARKDOWN_REFLINK_RE = /
855 MARKDOWN_REFLINK_RE = /
856 \[([^\[\]]+)\] # $text
856 \[([^\[\]]+)\] # $text
857 [ ]? # opt. space
857 [ ]? # opt. space
858 (?:\n[ ]*)? # one optional newline followed by spaces
858 (?:\n[ ]*)? # one optional newline followed by spaces
859 \[(.*?)\] # $id
859 \[(.*?)\] # $id
860 /x
860 /x
861
861
862 def inline_markdown_reflink( text )
862 def inline_markdown_reflink( text )
863 text.gsub!( MARKDOWN_REFLINK_RE ) do |m|
863 text.gsub!( MARKDOWN_REFLINK_RE ) do |m|
864 text, id = $~[1..2]
864 text, id = $~[1..2]
865
865
866 if id.empty?
866 if id.empty?
867 url, title = check_refs( text )
867 url, title = check_refs( text )
868 else
868 else
869 url, title = check_refs( id )
869 url, title = check_refs( id )
870 end
870 end
871
871
872 atts = " href=\"#{ url }\""
872 atts = " href=\"#{ url }\""
873 atts << " title=\"#{ title }\"" if title
873 atts << " title=\"#{ title }\"" if title
874 atts = shelve( atts )
874 atts = shelve( atts )
875
875
876 "<a#{ atts }>#{ text }</a>"
876 "<a#{ atts }>#{ text }</a>"
877 end
877 end
878 end
878 end
879
879
880 MARKDOWN_LINK_RE = /
880 MARKDOWN_LINK_RE = /
881 \[([^\[\]]+)\] # $text
881 \[([^\[\]]+)\] # $text
882 \( # open paren
882 \( # open paren
883 [ \t]* # opt space
883 [ \t]* # opt space
884 <?(.+?)>? # $href
884 <?(.+?)>? # $href
885 [ \t]* # opt space
885 [ \t]* # opt space
886 (?: # whole title
886 (?: # whole title
887 (['"]) # $quote
887 (['"]) # $quote
888 (.*?) # $title
888 (.*?) # $title
889 \3 # matching quote
889 \3 # matching quote
890 )? # title is optional
890 )? # title is optional
891 \)
891 \)
892 /x
892 /x
893
893
894 def inline_markdown_link( text )
894 def inline_markdown_link( text )
895 text.gsub!( MARKDOWN_LINK_RE ) do |m|
895 text.gsub!( MARKDOWN_LINK_RE ) do |m|
896 text, url, quote, title = $~[1..4]
896 text, url, quote, title = $~[1..4]
897
897
898 atts = " href=\"#{ url }\""
898 atts = " href=\"#{ url }\""
899 atts << " title=\"#{ title }\"" if title
899 atts << " title=\"#{ title }\"" if title
900 atts = shelve( atts )
900 atts = shelve( atts )
901
901
902 "<a#{ atts }>#{ text }</a>"
902 "<a#{ atts }>#{ text }</a>"
903 end
903 end
904 end
904 end
905
905
906 TEXTILE_REFS_RE = /(^ *)\[([^\[\n]+?)\](#{HYPERLINK})(?=\s|$)/
906 TEXTILE_REFS_RE = /(^ *)\[([^\[\n]+?)\](#{HYPERLINK})(?=\s|$)/
907 MARKDOWN_REFS_RE = /(^ *)\[([^\n]+?)\]:\s+<?(#{HYPERLINK})>?(?:\s+"((?:[^"]|\\")+)")?(?=\s|$)/m
907 MARKDOWN_REFS_RE = /(^ *)\[([^\n]+?)\]:\s+<?(#{HYPERLINK})>?(?:\s+"((?:[^"]|\\")+)")?(?=\s|$)/m
908
908
909 def refs( text )
909 def refs( text )
910 @rules.each do |rule_name|
910 @rules.each do |rule_name|
911 method( rule_name ).call( text ) if rule_name.to_s.match /^refs_/
911 method( rule_name ).call( text ) if rule_name.to_s.match /^refs_/
912 end
912 end
913 end
913 end
914
914
915 def refs_textile( text )
915 def refs_textile( text )
916 text.gsub!( TEXTILE_REFS_RE ) do |m|
916 text.gsub!( TEXTILE_REFS_RE ) do |m|
917 flag, url = $~[2..3]
917 flag, url = $~[2..3]
918 @urlrefs[flag.downcase] = [url, nil]
918 @urlrefs[flag.downcase] = [url, nil]
919 nil
919 nil
920 end
920 end
921 end
921 end
922
922
923 def refs_markdown( text )
923 def refs_markdown( text )
924 text.gsub!( MARKDOWN_REFS_RE ) do |m|
924 text.gsub!( MARKDOWN_REFS_RE ) do |m|
925 flag, url = $~[2..3]
925 flag, url = $~[2..3]
926 title = $~[6]
926 title = $~[6]
927 @urlrefs[flag.downcase] = [url, title]
927 @urlrefs[flag.downcase] = [url, title]
928 nil
928 nil
929 end
929 end
930 end
930 end
931
931
932 def check_refs( text )
932 def check_refs( text )
933 ret = @urlrefs[text.downcase] if text
933 ret = @urlrefs[text.downcase] if text
934 ret || [text, nil]
934 ret || [text, nil]
935 end
935 end
936
936
937 IMAGE_RE = /
937 IMAGE_RE = /
938 (>|\s|^) # start of line?
938 (>|\s|^) # start of line?
939 \! # opening
939 \! # opening
940 (\<|\=|\>)? # optional alignment atts
940 (\<|\=|\>)? # optional alignment atts
941 (#{C}) # optional style,class atts
941 (#{C}) # optional style,class atts
942 (?:\. )? # optional dot-space
942 (?:\. )? # optional dot-space
943 ([^\s(!]+?) # presume this is the src
943 ([^\s(!]+?) # presume this is the src
944 \s? # optional space
944 \s? # optional space
945 (?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title
945 (?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title
946 \! # closing
946 \! # closing
947 (?::#{ HYPERLINK })? # optional href
947 (?::#{ HYPERLINK })? # optional href
948 /x
948 /x
949
949
950 def inline_textile_image( text )
950 def inline_textile_image( text )
951 text.gsub!( IMAGE_RE ) do |m|
951 text.gsub!( IMAGE_RE ) do |m|
952 stln,algn,atts,url,title,href,href_a1,href_a2 = $~[1..8]
952 stln,algn,atts,url,title,href,href_a1,href_a2 = $~[1..8]
953 htmlesc title
953 htmlesc title
954 atts = pba( atts )
954 atts = pba( atts )
955 atts = " src=\"#{ htmlesc url.dup }\"#{ atts }"
955 atts = " src=\"#{ htmlesc url.dup }\"#{ atts }"
956 atts << " title=\"#{ title }\"" if title
956 atts << " title=\"#{ title }\"" if title
957 atts << " alt=\"#{ title }\""
957 atts << " alt=\"#{ title }\""
958 # size = @getimagesize($url);
958 # size = @getimagesize($url);
959 # if($size) $atts.= " $size[3]";
959 # if($size) $atts.= " $size[3]";
960
960
961 href, alt_title = check_refs( href ) if href
961 href, alt_title = check_refs( href ) if href
962 url, url_title = check_refs( url )
962 url, url_title = check_refs( url )
963
963
964 return m unless uri_with_safe_scheme?(url)
964 return m unless uri_with_safe_scheme?(url)
965
965
966 out = ''
966 out = ''
967 out << "<a#{ shelve( " href=\"#{ href }\"" ) }>" if href
967 out << "<a#{ shelve( " href=\"#{ href }\"" ) }>" if href
968 out << "<img#{ shelve( atts ) } />"
968 out << "<img#{ shelve( atts ) } />"
969 out << "</a>#{ href_a1 }#{ href_a2 }" if href
969 out << "</a>#{ href_a1 }#{ href_a2 }" if href
970
970
971 if algn
971 if algn
972 algn = h_align( algn )
972 algn = h_align( algn )
973 if stln == "<p>"
973 if stln == "<p>"
974 out = "<p style=\"float:#{ algn }\">#{ out }"
974 out = "<p style=\"float:#{ algn }\">#{ out }"
975 else
975 else
976 out = "#{ stln }<div style=\"float:#{ algn }\">#{ out }</div>"
976 out = "#{ stln }<span style=\"float:#{ algn }\">#{ out }</span>"
977 end
977 end
978 else
978 else
979 out = stln + out
979 out = stln + out
980 end
980 end
981
981
982 out
982 out
983 end
983 end
984 end
984 end
985
985
986 def shelve( val )
986 def shelve( val )
987 @shelf << val
987 @shelf << val
988 " :redsh##{ @shelf.length }:"
988 " :redsh##{ @shelf.length }:"
989 end
989 end
990
990
991 def retrieve( text )
991 def retrieve( text )
992 text.gsub!(/ :redsh#(\d+):/) do
992 text.gsub!(/ :redsh#(\d+):/) do
993 @shelf[$1.to_i - 1] || $&
993 @shelf[$1.to_i - 1] || $&
994 end
994 end
995 end
995 end
996
996
997 def incoming_entities( text )
997 def incoming_entities( text )
998 ## turn any incoming ampersands into a dummy character for now.
998 ## turn any incoming ampersands into a dummy character for now.
999 ## This uses a negative lookahead for alphanumerics followed by a semicolon,
999 ## This uses a negative lookahead for alphanumerics followed by a semicolon,
1000 ## implying an incoming html entity, to be skipped
1000 ## implying an incoming html entity, to be skipped
1001
1001
1002 text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" )
1002 text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" )
1003 end
1003 end
1004
1004
1005 def no_textile( text )
1005 def no_textile( text )
1006 text.gsub!( /(^|\s)==([^=]+.*?)==(\s|$)?/,
1006 text.gsub!( /(^|\s)==([^=]+.*?)==(\s|$)?/,
1007 '\1<notextile>\2</notextile>\3' )
1007 '\1<notextile>\2</notextile>\3' )
1008 text.gsub!( /^ *==([^=]+.*?)==/m,
1008 text.gsub!( /^ *==([^=]+.*?)==/m,
1009 '\1<notextile>\2</notextile>\3' )
1009 '\1<notextile>\2</notextile>\3' )
1010 end
1010 end
1011
1011
1012 def clean_white_space( text )
1012 def clean_white_space( text )
1013 # normalize line breaks
1013 # normalize line breaks
1014 text.gsub!( /\r\n/, "\n" )
1014 text.gsub!( /\r\n/, "\n" )
1015 text.gsub!( /\r/, "\n" )
1015 text.gsub!( /\r/, "\n" )
1016 text.gsub!( /\t/, ' ' )
1016 text.gsub!( /\t/, ' ' )
1017 text.gsub!( /^ +$/, '' )
1017 text.gsub!( /^ +$/, '' )
1018 text.gsub!( /\n{3,}/, "\n\n" )
1018 text.gsub!( /\n{3,}/, "\n\n" )
1019 text.gsub!( /"$/, "\" " )
1019 text.gsub!( /"$/, "\" " )
1020
1020
1021 # if entire document is indented, flush
1021 # if entire document is indented, flush
1022 # to the left side
1022 # to the left side
1023 flush_left text
1023 flush_left text
1024 end
1024 end
1025
1025
1026 def flush_left( text )
1026 def flush_left( text )
1027 indt = 0
1027 indt = 0
1028 if text =~ /^ /
1028 if text =~ /^ /
1029 while text !~ /^ {#{indt}}\S/
1029 while text !~ /^ {#{indt}}\S/
1030 indt += 1
1030 indt += 1
1031 end unless text.empty?
1031 end unless text.empty?
1032 if indt.nonzero?
1032 if indt.nonzero?
1033 text.gsub!( /^ {#{indt}}/, '' )
1033 text.gsub!( /^ {#{indt}}/, '' )
1034 end
1034 end
1035 end
1035 end
1036 end
1036 end
1037
1037
1038 def footnote_ref( text )
1038 def footnote_ref( text )
1039 text.gsub!( /\b\[([0-9]+?)\](\s)?/,
1039 text.gsub!( /\b\[([0-9]+?)\](\s)?/,
1040 '<sup><a href="#fn\1">\1</a></sup>\2' )
1040 '<sup><a href="#fn\1">\1</a></sup>\2' )
1041 end
1041 end
1042
1042
1043 OFFTAGS = /(code|pre|kbd|notextile)/
1043 OFFTAGS = /(code|pre|kbd|notextile)/
1044 OFFTAG_MATCH = /(?:(<\/#{ OFFTAGS }>)|(<#{ OFFTAGS }[^>]*>))(.*?)(?=<\/?#{ OFFTAGS }\W|\Z)/mi
1044 OFFTAG_MATCH = /(?:(<\/#{ OFFTAGS }>)|(<#{ OFFTAGS }[^>]*>))(.*?)(?=<\/?#{ OFFTAGS }\W|\Z)/mi
1045 OFFTAG_OPEN = /<#{ OFFTAGS }/
1045 OFFTAG_OPEN = /<#{ OFFTAGS }/
1046 OFFTAG_CLOSE = /<\/?#{ OFFTAGS }/
1046 OFFTAG_CLOSE = /<\/?#{ OFFTAGS }/
1047 HASTAG_MATCH = /(<\/?\w[^\n]*?>)/m
1047 HASTAG_MATCH = /(<\/?\w[^\n]*?>)/m
1048 ALLTAG_MATCH = /(<\/?\w[^\n]*?>)|.*?(?=<\/?\w[^\n]*?>|$)/m
1048 ALLTAG_MATCH = /(<\/?\w[^\n]*?>)|.*?(?=<\/?\w[^\n]*?>|$)/m
1049
1049
1050 def glyphs_textile( text, level = 0 )
1050 def glyphs_textile( text, level = 0 )
1051 if text !~ HASTAG_MATCH
1051 if text !~ HASTAG_MATCH
1052 pgl text
1052 pgl text
1053 footnote_ref text
1053 footnote_ref text
1054 else
1054 else
1055 codepre = 0
1055 codepre = 0
1056 text.gsub!( ALLTAG_MATCH ) do |line|
1056 text.gsub!( ALLTAG_MATCH ) do |line|
1057 ## matches are off if we're between <code>, <pre> etc.
1057 ## matches are off if we're between <code>, <pre> etc.
1058 if $1
1058 if $1
1059 if line =~ OFFTAG_OPEN
1059 if line =~ OFFTAG_OPEN
1060 codepre += 1
1060 codepre += 1
1061 elsif line =~ OFFTAG_CLOSE
1061 elsif line =~ OFFTAG_CLOSE
1062 codepre -= 1
1062 codepre -= 1
1063 codepre = 0 if codepre < 0
1063 codepre = 0 if codepre < 0
1064 end
1064 end
1065 elsif codepre.zero?
1065 elsif codepre.zero?
1066 glyphs_textile( line, level + 1 )
1066 glyphs_textile( line, level + 1 )
1067 else
1067 else
1068 htmlesc( line, :NoQuotes )
1068 htmlesc( line, :NoQuotes )
1069 end
1069 end
1070 # p [level, codepre, line]
1070 # p [level, codepre, line]
1071
1071
1072 line
1072 line
1073 end
1073 end
1074 end
1074 end
1075 end
1075 end
1076
1076
1077 def rip_offtags( text, escape_aftertag=true, escape_line=true )
1077 def rip_offtags( text, escape_aftertag=true, escape_line=true )
1078 if text =~ /<.*>/
1078 if text =~ /<.*>/
1079 ## strip and encode <pre> content
1079 ## strip and encode <pre> content
1080 codepre, used_offtags = 0, {}
1080 codepre, used_offtags = 0, {}
1081 text.gsub!( OFFTAG_MATCH ) do |line|
1081 text.gsub!( OFFTAG_MATCH ) do |line|
1082 if $3
1082 if $3
1083 first, offtag, aftertag = $3, $4, $5
1083 first, offtag, aftertag = $3, $4, $5
1084 codepre += 1
1084 codepre += 1
1085 used_offtags[offtag] = true
1085 used_offtags[offtag] = true
1086 if codepre - used_offtags.length > 0
1086 if codepre - used_offtags.length > 0
1087 htmlesc( line, :NoQuotes ) if escape_line
1087 htmlesc( line, :NoQuotes ) if escape_line
1088 @pre_list.last << line
1088 @pre_list.last << line
1089 line = ""
1089 line = ""
1090 else
1090 else
1091 ### htmlesc is disabled between CODE tags which will be parsed with highlighter
1091 ### htmlesc is disabled between CODE tags which will be parsed with highlighter
1092 ### Regexp in formatter.rb is : /<code\s+class="(\w+)">\s?(.+)/m
1092 ### Regexp in formatter.rb is : /<code\s+class="(\w+)">\s?(.+)/m
1093 ### NB: some changes were made not to use $N variables, because we use "match"
1093 ### NB: some changes were made not to use $N variables, because we use "match"
1094 ### and it breaks following lines
1094 ### and it breaks following lines
1095 htmlesc( aftertag, :NoQuotes ) if aftertag && escape_aftertag && !first.match(/<code\s+class="(\w+)">/)
1095 htmlesc( aftertag, :NoQuotes ) if aftertag && escape_aftertag && !first.match(/<code\s+class="(\w+)">/)
1096 line = "<redpre##{ @pre_list.length }>"
1096 line = "<redpre##{ @pre_list.length }>"
1097 first.match(/<#{ OFFTAGS }([^>]*)>/)
1097 first.match(/<#{ OFFTAGS }([^>]*)>/)
1098 tag = $1
1098 tag = $1
1099 $2.to_s.match(/(class\=("[^"]+"|'[^']+'))/i)
1099 $2.to_s.match(/(class\=("[^"]+"|'[^']+'))/i)
1100 tag << " #{$1}" if $1
1100 tag << " #{$1}" if $1
1101 @pre_list << "<#{ tag }>#{ aftertag }"
1101 @pre_list << "<#{ tag }>#{ aftertag }"
1102 end
1102 end
1103 elsif $1 and codepre > 0
1103 elsif $1 and codepre > 0
1104 if codepre - used_offtags.length > 0
1104 if codepre - used_offtags.length > 0
1105 htmlesc( line, :NoQuotes ) if escape_line
1105 htmlesc( line, :NoQuotes ) if escape_line
1106 @pre_list.last << line
1106 @pre_list.last << line
1107 line = ""
1107 line = ""
1108 end
1108 end
1109 codepre -= 1 unless codepre.zero?
1109 codepre -= 1 unless codepre.zero?
1110 used_offtags = {} if codepre.zero?
1110 used_offtags = {} if codepre.zero?
1111 end
1111 end
1112 line
1112 line
1113 end
1113 end
1114 end
1114 end
1115 text
1115 text
1116 end
1116 end
1117
1117
1118 def smooth_offtags( text )
1118 def smooth_offtags( text )
1119 unless @pre_list.empty?
1119 unless @pre_list.empty?
1120 ## replace <pre> content
1120 ## replace <pre> content
1121 text.gsub!( /<redpre#(\d+)>/ ) { @pre_list[$1.to_i] }
1121 text.gsub!( /<redpre#(\d+)>/ ) { @pre_list[$1.to_i] }
1122 end
1122 end
1123 end
1123 end
1124
1124
1125 def inline( text )
1125 def inline( text )
1126 [/^inline_/, /^glyphs_/].each do |meth_re|
1126 [/^inline_/, /^glyphs_/].each do |meth_re|
1127 @rules.each do |rule_name|
1127 @rules.each do |rule_name|
1128 method( rule_name ).call( text ) if rule_name.to_s.match( meth_re )
1128 method( rule_name ).call( text ) if rule_name.to_s.match( meth_re )
1129 end
1129 end
1130 end
1130 end
1131 end
1131 end
1132
1132
1133 def h_align( text )
1133 def h_align( text )
1134 H_ALGN_VALS[text]
1134 H_ALGN_VALS[text]
1135 end
1135 end
1136
1136
1137 def v_align( text )
1137 def v_align( text )
1138 V_ALGN_VALS[text]
1138 V_ALGN_VALS[text]
1139 end
1139 end
1140
1140
1141 def textile_popup_help( name, windowW, windowH )
1141 def textile_popup_help( name, windowW, windowH )
1142 ' <a target="_blank" href="http://hobix.com/textile/#' + helpvar + '" onclick="window.open(this.href, \'popupwindow\', \'width=' + windowW + ',height=' + windowH + ',scrollbars,resizable\'); return false;">' + name + '</a><br />'
1142 ' <a target="_blank" href="http://hobix.com/textile/#' + helpvar + '" onclick="window.open(this.href, \'popupwindow\', \'width=' + windowW + ',height=' + windowH + ',scrollbars,resizable\'); return false;">' + name + '</a><br />'
1143 end
1143 end
1144
1144
1145 # HTML cleansing stuff
1145 # HTML cleansing stuff
1146 BASIC_TAGS = {
1146 BASIC_TAGS = {
1147 'a' => ['href', 'title'],
1147 'a' => ['href', 'title'],
1148 'img' => ['src', 'alt', 'title'],
1148 'img' => ['src', 'alt', 'title'],
1149 'br' => [],
1149 'br' => [],
1150 'i' => nil,
1150 'i' => nil,
1151 'u' => nil,
1151 'u' => nil,
1152 'b' => nil,
1152 'b' => nil,
1153 'pre' => nil,
1153 'pre' => nil,
1154 'kbd' => nil,
1154 'kbd' => nil,
1155 'code' => ['lang'],
1155 'code' => ['lang'],
1156 'cite' => nil,
1156 'cite' => nil,
1157 'strong' => nil,
1157 'strong' => nil,
1158 'em' => nil,
1158 'em' => nil,
1159 'ins' => nil,
1159 'ins' => nil,
1160 'sup' => nil,
1160 'sup' => nil,
1161 'sub' => nil,
1161 'sub' => nil,
1162 'del' => nil,
1162 'del' => nil,
1163 'table' => nil,
1163 'table' => nil,
1164 'tr' => nil,
1164 'tr' => nil,
1165 'td' => ['colspan', 'rowspan'],
1165 'td' => ['colspan', 'rowspan'],
1166 'th' => nil,
1166 'th' => nil,
1167 'ol' => nil,
1167 'ol' => nil,
1168 'ul' => nil,
1168 'ul' => nil,
1169 'li' => nil,
1169 'li' => nil,
1170 'p' => nil,
1170 'p' => nil,
1171 'h1' => nil,
1171 'h1' => nil,
1172 'h2' => nil,
1172 'h2' => nil,
1173 'h3' => nil,
1173 'h3' => nil,
1174 'h4' => nil,
1174 'h4' => nil,
1175 'h5' => nil,
1175 'h5' => nil,
1176 'h6' => nil,
1176 'h6' => nil,
1177 'blockquote' => ['cite']
1177 'blockquote' => ['cite']
1178 }
1178 }
1179
1179
1180 def clean_html( text, tags = BASIC_TAGS )
1180 def clean_html( text, tags = BASIC_TAGS )
1181 text.gsub!( /<!\[CDATA\[/, '' )
1181 text.gsub!( /<!\[CDATA\[/, '' )
1182 text.gsub!( /<(\/*)(\w+)([^>]*)>/ ) do
1182 text.gsub!( /<(\/*)(\w+)([^>]*)>/ ) do
1183 raw = $~
1183 raw = $~
1184 tag = raw[2].downcase
1184 tag = raw[2].downcase
1185 if tags.has_key? tag
1185 if tags.has_key? tag
1186 pcs = [tag]
1186 pcs = [tag]
1187 tags[tag].each do |prop|
1187 tags[tag].each do |prop|
1188 ['"', "'", ''].each do |q|
1188 ['"', "'", ''].each do |q|
1189 q2 = ( q != '' ? q : '\s' )
1189 q2 = ( q != '' ? q : '\s' )
1190 if raw[3] =~ /#{prop}\s*=\s*#{q}([^#{q2}]+)#{q}/i
1190 if raw[3] =~ /#{prop}\s*=\s*#{q}([^#{q2}]+)#{q}/i
1191 attrv = $1
1191 attrv = $1
1192 next if prop == 'src' and attrv =~ %r{^(?!http)\w+:}
1192 next if prop == 'src' and attrv =~ %r{^(?!http)\w+:}
1193 pcs << "#{prop}=\"#{$1.gsub('"', '\\"')}\""
1193 pcs << "#{prop}=\"#{$1.gsub('"', '\\"')}\""
1194 break
1194 break
1195 end
1195 end
1196 end
1196 end
1197 end if tags[tag]
1197 end if tags[tag]
1198 "<#{raw[1]}#{pcs.join " "}>"
1198 "<#{raw[1]}#{pcs.join " "}>"
1199 else
1199 else
1200 " "
1200 " "
1201 end
1201 end
1202 end
1202 end
1203 end
1203 end
1204
1204
1205 ALLOWED_TAGS = %w(redpre pre code notextile)
1205 ALLOWED_TAGS = %w(redpre pre code notextile)
1206
1206
1207 def escape_html_tags(text)
1207 def escape_html_tags(text)
1208 text.gsub!(%r{<(\/?([!\w]+)[^<>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "&lt;#{$1}#{'&gt;' unless $3.blank?}" }
1208 text.gsub!(%r{<(\/?([!\w]+)[^<>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "&lt;#{$1}#{'&gt;' unless $3.blank?}" }
1209 end
1209 end
1210 end
1210 end
1211
1211
@@ -1,1541 +1,1541
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
4 # Copyright (C) 2006-2016 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program 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 this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require File.expand_path('../../../test_helper', __FILE__)
20 require File.expand_path('../../../test_helper', __FILE__)
21
21
22 class ApplicationHelperTest < ActionView::TestCase
22 class ApplicationHelperTest < ActionView::TestCase
23 include Redmine::I18n
23 include Redmine::I18n
24 include ERB::Util
24 include ERB::Util
25 include Rails.application.routes.url_helpers
25 include Rails.application.routes.url_helpers
26
26
27 fixtures :projects, :roles, :enabled_modules, :users,
27 fixtures :projects, :roles, :enabled_modules, :users,
28 :email_addresses,
28 :email_addresses,
29 :repositories, :changesets,
29 :repositories, :changesets,
30 :projects_trackers,
30 :projects_trackers,
31 :trackers, :issue_statuses, :issues, :versions, :documents,
31 :trackers, :issue_statuses, :issues, :versions, :documents,
32 :wikis, :wiki_pages, :wiki_contents,
32 :wikis, :wiki_pages, :wiki_contents,
33 :boards, :messages, :news,
33 :boards, :messages, :news,
34 :attachments, :enumerations
34 :attachments, :enumerations
35
35
36 def setup
36 def setup
37 super
37 super
38 set_tmp_attachments_directory
38 set_tmp_attachments_directory
39 @russian_test = "\xd1\x82\xd0\xb5\xd1\x81\xd1\x82".force_encoding('UTF-8')
39 @russian_test = "\xd1\x82\xd0\xb5\xd1\x81\xd1\x82".force_encoding('UTF-8')
40 end
40 end
41
41
42 test "#link_to_if_authorized for authorized user should allow using the :controller and :action for the target link" do
42 test "#link_to_if_authorized for authorized user should allow using the :controller and :action for the target link" do
43 User.current = User.find_by_login('admin')
43 User.current = User.find_by_login('admin')
44
44
45 @project = Issue.first.project # Used by helper
45 @project = Issue.first.project # Used by helper
46 response = link_to_if_authorized('By controller/actionr',
46 response = link_to_if_authorized('By controller/actionr',
47 {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
47 {:controller => 'issues', :action => 'edit', :id => Issue.first.id})
48 assert_match /href/, response
48 assert_match /href/, response
49 end
49 end
50
50
51 test "#link_to_if_authorized for unauthorized user should display nothing if user isn't authorized" do
51 test "#link_to_if_authorized for unauthorized user should display nothing if user isn't authorized" do
52 User.current = User.find_by_login('dlopper')
52 User.current = User.find_by_login('dlopper')
53 @project = Project.find('private-child')
53 @project = Project.find('private-child')
54 issue = @project.issues.first
54 issue = @project.issues.first
55 assert !issue.visible?
55 assert !issue.visible?
56
56
57 response = link_to_if_authorized('Never displayed',
57 response = link_to_if_authorized('Never displayed',
58 {:controller => 'issues', :action => 'show', :id => issue})
58 {:controller => 'issues', :action => 'show', :id => issue})
59 assert_nil response
59 assert_nil response
60 end
60 end
61
61
62 def test_auto_links
62 def test_auto_links
63 to_test = {
63 to_test = {
64 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
64 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
65 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
65 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
66 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
66 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
67 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
67 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
68 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
68 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
69 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
69 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
70 '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>.',
70 '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>.',
71 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
71 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
72 '(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>)',
72 '(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>)',
73 '(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>)',
73 '(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>)',
74 '(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>).',
74 '(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>).',
75 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
75 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
76 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
76 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
77 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
77 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
78 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
78 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
79 '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>',
79 '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>',
80 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
80 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
81 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
81 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
82 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
82 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
83 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
83 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
84 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
84 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
85 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
85 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
86 # two exclamation marks
86 # two exclamation marks
87 'http://example.net/path!602815048C7B5C20!302.html' => '<a class="external" href="http://example.net/path!602815048C7B5C20!302.html">http://example.net/path!602815048C7B5C20!302.html</a>',
87 'http://example.net/path!602815048C7B5C20!302.html' => '<a class="external" href="http://example.net/path!602815048C7B5C20!302.html">http://example.net/path!602815048C7B5C20!302.html</a>',
88 # escaping
88 # escaping
89 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo&quot;bar</a>',
89 'http://foo"bar' => '<a class="external" href="http://foo&quot;bar">http://foo&quot;bar</a>',
90 # wrap in angle brackets
90 # wrap in angle brackets
91 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;',
91 '<http://foo.bar>' => '&lt;<a class="external" href="http://foo.bar">http://foo.bar</a>&gt;',
92 # invalid urls
92 # invalid urls
93 'http://' => 'http://',
93 'http://' => 'http://',
94 'www.' => 'www.',
94 'www.' => 'www.',
95 'test-www.bar.com' => 'test-www.bar.com',
95 'test-www.bar.com' => 'test-www.bar.com',
96 }
96 }
97 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
97 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
98 end
98 end
99
99
100 def test_auto_links_with_non_ascii_characters
100 def test_auto_links_with_non_ascii_characters
101 to_test = {
101 to_test = {
102 "http://foo.bar/#{@russian_test}" =>
102 "http://foo.bar/#{@russian_test}" =>
103 %|<a class="external" href="http://foo.bar/#{@russian_test}">http://foo.bar/#{@russian_test}</a>|
103 %|<a class="external" href="http://foo.bar/#{@russian_test}">http://foo.bar/#{@russian_test}</a>|
104 }
104 }
105 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
105 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
106 end
106 end
107
107
108 def test_auto_mailto
108 def test_auto_mailto
109 to_test = {
109 to_test = {
110 'test@foo.bar' => '<a class="email" href="mailto:test@foo.bar">test@foo.bar</a>',
110 'test@foo.bar' => '<a class="email" href="mailto:test@foo.bar">test@foo.bar</a>',
111 'test@www.foo.bar' => '<a class="email" href="mailto:test@www.foo.bar">test@www.foo.bar</a>',
111 'test@www.foo.bar' => '<a class="email" href="mailto:test@www.foo.bar">test@www.foo.bar</a>',
112 }
112 }
113 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
113 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
114 end
114 end
115
115
116 def test_inline_images
116 def test_inline_images
117 to_test = {
117 to_test = {
118 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
118 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
119 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
119 'floating !>http://foo.bar/image.jpg!' => 'floating <span style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></span>',
120 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
120 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
121 'with style !{width:100px;height:100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" style="width:100px;height:100px;" alt="" />',
121 'with style !{width:100px;height:100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" style="width:100px;height:100px;" alt="" />',
122 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a title" alt="This is a title" />',
122 'with title !http://foo.bar/image.jpg(This is a title)!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a title" alt="This is a title" />',
123 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a double-quoted &quot;title&quot;" alt="This is a double-quoted &quot;title&quot;" />',
123 'with title !http://foo.bar/image.jpg(This is a double-quoted "title")!' => 'with title <img src="http://foo.bar/image.jpg" title="This is a double-quoted &quot;title&quot;" alt="This is a double-quoted &quot;title&quot;" />',
124 }
124 }
125 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
125 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
126 end
126 end
127
127
128 def test_inline_images_inside_tags
128 def test_inline_images_inside_tags
129 raw = <<-RAW
129 raw = <<-RAW
130 h1. !foo.png! Heading
130 h1. !foo.png! Heading
131
131
132 Centered image:
132 Centered image:
133
133
134 p=. !bar.gif!
134 p=. !bar.gif!
135 RAW
135 RAW
136
136
137 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
137 assert textilizable(raw).include?('<img src="foo.png" alt="" />')
138 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
138 assert textilizable(raw).include?('<img src="bar.gif" alt="" />')
139 end
139 end
140
140
141 def test_attached_images
141 def test_attached_images
142 to_test = {
142 to_test = {
143 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" />',
143 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" />',
144 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" />',
144 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" />',
145 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
145 'No match: !ogo.gif!' => 'No match: <img src="ogo.gif" alt="" />',
146 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
146 'No match: !ogo.GIF!' => 'No match: <img src="ogo.GIF" alt="" />',
147 # link image
147 # link image
148 '!logo.gif!:http://foo.bar/' => '<a href="http://foo.bar/"><img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" /></a>',
148 '!logo.gif!:http://foo.bar/' => '<a href="http://foo.bar/"><img src="/attachments/download/3/logo.gif" title="This is a logo" alt="This is a logo" /></a>',
149 }
149 }
150 attachments = Attachment.all
150 attachments = Attachment.all
151 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
151 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
152 end
152 end
153
153
154 def test_attached_images_with_textile_and_non_ascii_filename
154 def test_attached_images_with_textile_and_non_ascii_filename
155 attachment = Attachment.generate!(:filename => 'cafΓ©.jpg')
155 attachment = Attachment.generate!(:filename => 'cafΓ©.jpg')
156 with_settings :text_formatting => 'textile' do
156 with_settings :text_formatting => 'textile' do
157 assert_include %(<img src="/attachments/download/#{attachment.id}/caf%C3%A9.jpg" alt="" />),
157 assert_include %(<img src="/attachments/download/#{attachment.id}/caf%C3%A9.jpg" alt="" />),
158 textilizable("!cafΓ©.jpg!)", :attachments => [attachment])
158 textilizable("!cafΓ©.jpg!)", :attachments => [attachment])
159 end
159 end
160 end
160 end
161
161
162 def test_attached_images_with_markdown_and_non_ascii_filename
162 def test_attached_images_with_markdown_and_non_ascii_filename
163 skip unless Object.const_defined?(:Redcarpet)
163 skip unless Object.const_defined?(:Redcarpet)
164
164
165 attachment = Attachment.generate!(:filename => 'cafΓ©.jpg')
165 attachment = Attachment.generate!(:filename => 'cafΓ©.jpg')
166 with_settings :text_formatting => 'markdown' do
166 with_settings :text_formatting => 'markdown' do
167 assert_include %(<img src="/attachments/download/#{attachment.id}/caf%C3%A9.jpg" alt="" />),
167 assert_include %(<img src="/attachments/download/#{attachment.id}/caf%C3%A9.jpg" alt="" />),
168 textilizable("![](cafΓ©.jpg)", :attachments => [attachment])
168 textilizable("![](cafΓ©.jpg)", :attachments => [attachment])
169 end
169 end
170 end
170 end
171
171
172 def test_attached_images_filename_extension
172 def test_attached_images_filename_extension
173 set_tmp_attachments_directory
173 set_tmp_attachments_directory
174 a1 = Attachment.new(
174 a1 = Attachment.new(
175 :container => Issue.find(1),
175 :container => Issue.find(1),
176 :file => mock_file_with_options({:original_filename => "testtest.JPG"}),
176 :file => mock_file_with_options({:original_filename => "testtest.JPG"}),
177 :author => User.find(1))
177 :author => User.find(1))
178 assert a1.save
178 assert a1.save
179 assert_equal "testtest.JPG", a1.filename
179 assert_equal "testtest.JPG", a1.filename
180 assert_equal "image/jpeg", a1.content_type
180 assert_equal "image/jpeg", a1.content_type
181 assert a1.image?
181 assert a1.image?
182
182
183 a2 = Attachment.new(
183 a2 = Attachment.new(
184 :container => Issue.find(1),
184 :container => Issue.find(1),
185 :file => mock_file_with_options({:original_filename => "testtest.jpeg"}),
185 :file => mock_file_with_options({:original_filename => "testtest.jpeg"}),
186 :author => User.find(1))
186 :author => User.find(1))
187 assert a2.save
187 assert a2.save
188 assert_equal "testtest.jpeg", a2.filename
188 assert_equal "testtest.jpeg", a2.filename
189 assert_equal "image/jpeg", a2.content_type
189 assert_equal "image/jpeg", a2.content_type
190 assert a2.image?
190 assert a2.image?
191
191
192 a3 = Attachment.new(
192 a3 = Attachment.new(
193 :container => Issue.find(1),
193 :container => Issue.find(1),
194 :file => mock_file_with_options({:original_filename => "testtest.JPE"}),
194 :file => mock_file_with_options({:original_filename => "testtest.JPE"}),
195 :author => User.find(1))
195 :author => User.find(1))
196 assert a3.save
196 assert a3.save
197 assert_equal "testtest.JPE", a3.filename
197 assert_equal "testtest.JPE", a3.filename
198 assert_equal "image/jpeg", a3.content_type
198 assert_equal "image/jpeg", a3.content_type
199 assert a3.image?
199 assert a3.image?
200
200
201 a4 = Attachment.new(
201 a4 = Attachment.new(
202 :container => Issue.find(1),
202 :container => Issue.find(1),
203 :file => mock_file_with_options({:original_filename => "Testtest.BMP"}),
203 :file => mock_file_with_options({:original_filename => "Testtest.BMP"}),
204 :author => User.find(1))
204 :author => User.find(1))
205 assert a4.save
205 assert a4.save
206 assert_equal "Testtest.BMP", a4.filename
206 assert_equal "Testtest.BMP", a4.filename
207 assert_equal "image/x-ms-bmp", a4.content_type
207 assert_equal "image/x-ms-bmp", a4.content_type
208 assert a4.image?
208 assert a4.image?
209
209
210 to_test = {
210 to_test = {
211 'Inline image: !testtest.jpg!' =>
211 'Inline image: !testtest.jpg!' =>
212 'Inline image: <img src="/attachments/download/' + a1.id.to_s + '/testtest.JPG" alt="" />',
212 'Inline image: <img src="/attachments/download/' + a1.id.to_s + '/testtest.JPG" alt="" />',
213 'Inline image: !testtest.jpeg!' =>
213 'Inline image: !testtest.jpeg!' =>
214 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testtest.jpeg" alt="" />',
214 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testtest.jpeg" alt="" />',
215 'Inline image: !testtest.jpe!' =>
215 'Inline image: !testtest.jpe!' =>
216 'Inline image: <img src="/attachments/download/' + a3.id.to_s + '/testtest.JPE" alt="" />',
216 'Inline image: <img src="/attachments/download/' + a3.id.to_s + '/testtest.JPE" alt="" />',
217 'Inline image: !testtest.bmp!' =>
217 'Inline image: !testtest.bmp!' =>
218 'Inline image: <img src="/attachments/download/' + a4.id.to_s + '/Testtest.BMP" alt="" />',
218 'Inline image: <img src="/attachments/download/' + a4.id.to_s + '/Testtest.BMP" alt="" />',
219 }
219 }
220
220
221 attachments = [a1, a2, a3, a4]
221 attachments = [a1, a2, a3, a4]
222 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
222 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
223 end
223 end
224
224
225 def test_attached_images_should_read_later
225 def test_attached_images_should_read_later
226 set_fixtures_attachments_directory
226 set_fixtures_attachments_directory
227 a1 = Attachment.find(16)
227 a1 = Attachment.find(16)
228 assert_equal "testfile.png", a1.filename
228 assert_equal "testfile.png", a1.filename
229 assert a1.readable?
229 assert a1.readable?
230 assert (! a1.visible?(User.anonymous))
230 assert (! a1.visible?(User.anonymous))
231 assert a1.visible?(User.find(2))
231 assert a1.visible?(User.find(2))
232 a2 = Attachment.find(17)
232 a2 = Attachment.find(17)
233 assert_equal "testfile.PNG", a2.filename
233 assert_equal "testfile.PNG", a2.filename
234 assert a2.readable?
234 assert a2.readable?
235 assert (! a2.visible?(User.anonymous))
235 assert (! a2.visible?(User.anonymous))
236 assert a2.visible?(User.find(2))
236 assert a2.visible?(User.find(2))
237 assert a1.created_on < a2.created_on
237 assert a1.created_on < a2.created_on
238
238
239 to_test = {
239 to_test = {
240 'Inline image: !testfile.png!' =>
240 'Inline image: !testfile.png!' =>
241 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" />',
241 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" />',
242 'Inline image: !Testfile.PNG!' =>
242 'Inline image: !Testfile.PNG!' =>
243 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" />',
243 'Inline image: <img src="/attachments/download/' + a2.id.to_s + '/testfile.PNG" alt="" />',
244 }
244 }
245 attachments = [a1, a2]
245 attachments = [a1, a2]
246 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
246 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
247 set_tmp_attachments_directory
247 set_tmp_attachments_directory
248 end
248 end
249
249
250 def test_textile_external_links
250 def test_textile_external_links
251 to_test = {
251 to_test = {
252 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
252 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
253 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
253 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
254 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
254 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
255 '"link (Link title with "double-quotes")":http://foo.bar' => '<a href="http://foo.bar" title="Link title with &quot;double-quotes&quot;" class="external">link</a>',
255 '"link (Link title with "double-quotes")":http://foo.bar' => '<a href="http://foo.bar" title="Link title with &quot;double-quotes&quot;" class="external">link</a>',
256 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
256 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
257 # no multiline link text
257 # no multiline link text
258 "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 />and another on a second line\":test",
258 "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 />and another on a second line\":test",
259 # mailto link
259 # mailto link
260 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
260 "\"system administrator\":mailto:sysadmin@example.com?subject=redmine%20permissions" => "<a href=\"mailto:sysadmin@example.com?subject=redmine%20permissions\">system administrator</a>",
261 # two exclamation marks
261 # two exclamation marks
262 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
262 '"a link":http://example.net/path!602815048C7B5C20!302.html' => '<a href="http://example.net/path!602815048C7B5C20!302.html" class="external">a link</a>',
263 # escaping
263 # escaping
264 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
264 '"test":http://foo"bar' => '<a href="http://foo&quot;bar" class="external">test</a>',
265 }
265 }
266 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
266 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
267 end
267 end
268
268
269 def test_textile_external_links_with_non_ascii_characters
269 def test_textile_external_links_with_non_ascii_characters
270 to_test = {
270 to_test = {
271 %|This is a "link":http://foo.bar/#{@russian_test}| =>
271 %|This is a "link":http://foo.bar/#{@russian_test}| =>
272 %|This is a <a href="http://foo.bar/#{@russian_test}" class="external">link</a>|
272 %|This is a <a href="http://foo.bar/#{@russian_test}" class="external">link</a>|
273 }
273 }
274 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
274 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
275 end
275 end
276
276
277 def test_redmine_links
277 def test_redmine_links
278 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
278 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
279 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
279 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
280 note_link = link_to('#3-14', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
280 note_link = link_to('#3-14', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
281 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
281 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
282 note_link2 = link_to('#3#note-14', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
282 note_link2 = link_to('#3#note-14', {:controller => 'issues', :action => 'show', :id => 3, :anchor => 'note-14'},
283 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
283 :class => Issue.find(3).css_classes, :title => 'Bug: Error 281 when updating a recipe (New)')
284
284
285 revision_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
285 revision_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
286 :class => 'changeset', :title => 'My very first commit do not escaping #<>&')
286 :class => 'changeset', :title => 'My very first commit do not escaping #<>&')
287 revision_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
287 revision_link2 = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
288 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
288 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
289
289
290 changeset_link2 = link_to('691322a8eb01e11fd7',
290 changeset_link2 = link_to('691322a8eb01e11fd7',
291 {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
291 {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
292 :class => 'changeset', :title => 'My very first commit do not escaping #<>&')
292 :class => 'changeset', :title => 'My very first commit do not escaping #<>&')
293
293
294 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
294 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
295 :class => 'document')
295 :class => 'document')
296
296
297 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
297 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
298 :class => 'version')
298 :class => 'version')
299
299
300 board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'}
300 board_url = {:controller => 'boards', :action => 'show', :id => 2, :project_id => 'ecookbook'}
301
301
302 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
302 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
303
303
304 news_url = {:controller => 'news', :action => 'show', :id => 1}
304 news_url = {:controller => 'news', :action => 'show', :id => 1}
305
305
306 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
306 project_url = {:controller => 'projects', :action => 'show', :id => 'subproject1'}
307
307
308 source_url = '/projects/ecookbook/repository/entry/some/file'
308 source_url = '/projects/ecookbook/repository/entry/some/file'
309 source_url_with_rev = '/projects/ecookbook/repository/revisions/52/entry/some/file'
309 source_url_with_rev = '/projects/ecookbook/repository/revisions/52/entry/some/file'
310 source_url_with_ext = '/projects/ecookbook/repository/entry/some/file.ext'
310 source_url_with_ext = '/projects/ecookbook/repository/entry/some/file.ext'
311 source_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/entry/some/file.ext'
311 source_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/entry/some/file.ext'
312 source_url_with_branch = '/projects/ecookbook/repository/revisions/branch/entry/some/file'
312 source_url_with_branch = '/projects/ecookbook/repository/revisions/branch/entry/some/file'
313
313
314 export_url = '/projects/ecookbook/repository/raw/some/file'
314 export_url = '/projects/ecookbook/repository/raw/some/file'
315 export_url_with_rev = '/projects/ecookbook/repository/revisions/52/raw/some/file'
315 export_url_with_rev = '/projects/ecookbook/repository/revisions/52/raw/some/file'
316 export_url_with_ext = '/projects/ecookbook/repository/raw/some/file.ext'
316 export_url_with_ext = '/projects/ecookbook/repository/raw/some/file.ext'
317 export_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/raw/some/file.ext'
317 export_url_with_rev_and_ext = '/projects/ecookbook/repository/revisions/52/raw/some/file.ext'
318 export_url_with_branch = '/projects/ecookbook/repository/revisions/branch/raw/some/file'
318 export_url_with_branch = '/projects/ecookbook/repository/revisions/branch/raw/some/file'
319
319
320 to_test = {
320 to_test = {
321 # tickets
321 # tickets
322 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
322 '#3, [#3], (#3) and #3.' => "#{issue_link}, [#{issue_link}], (#{issue_link}) and #{issue_link}.",
323 # ticket notes
323 # ticket notes
324 '#3-14' => note_link,
324 '#3-14' => note_link,
325 '#3#note-14' => note_link2,
325 '#3#note-14' => note_link2,
326 # should not ignore leading zero
326 # should not ignore leading zero
327 '#03' => '#03',
327 '#03' => '#03',
328 # changesets
328 # changesets
329 'r1' => revision_link,
329 'r1' => revision_link,
330 'r1.' => "#{revision_link}.",
330 'r1.' => "#{revision_link}.",
331 'r1, r2' => "#{revision_link}, #{revision_link2}",
331 'r1, r2' => "#{revision_link}, #{revision_link2}",
332 'r1,r2' => "#{revision_link},#{revision_link2}",
332 'r1,r2' => "#{revision_link},#{revision_link2}",
333 'commit:691322a8eb01e11fd7' => changeset_link2,
333 'commit:691322a8eb01e11fd7' => changeset_link2,
334 # documents
334 # documents
335 'document#1' => document_link,
335 'document#1' => document_link,
336 'document:"Test document"' => document_link,
336 'document:"Test document"' => document_link,
337 # versions
337 # versions
338 'version#2' => version_link,
338 'version#2' => version_link,
339 'version:1.0' => version_link,
339 'version:1.0' => version_link,
340 'version:"1.0"' => version_link,
340 'version:"1.0"' => version_link,
341 # source
341 # source
342 'source:some/file' => link_to('source:some/file', source_url, :class => 'source'),
342 'source:some/file' => link_to('source:some/file', source_url, :class => 'source'),
343 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
343 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
344 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
344 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
345 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
345 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
346 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
346 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
347 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
347 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
348 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
348 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
349 'source:/some/file@52' => link_to('source:/some/file@52', source_url_with_rev, :class => 'source'),
349 'source:/some/file@52' => link_to('source:/some/file@52', source_url_with_rev, :class => 'source'),
350 'source:/some/file@branch' => link_to('source:/some/file@branch', source_url_with_branch, :class => 'source'),
350 'source:/some/file@branch' => link_to('source:/some/file@branch', source_url_with_branch, :class => 'source'),
351 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_rev_and_ext, :class => 'source'),
351 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_rev_and_ext, :class => 'source'),
352 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url + "#L110", :class => 'source'),
352 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url + "#L110", :class => 'source'),
353 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext + "#L110", :class => 'source'),
353 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext + "#L110", :class => 'source'),
354 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url_with_rev + "#L110", :class => 'source'),
354 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url_with_rev + "#L110", :class => 'source'),
355 # export
355 # export
356 'export:/some/file' => link_to('export:/some/file', export_url, :class => 'source download'),
356 'export:/some/file' => link_to('export:/some/file', export_url, :class => 'source download'),
357 'export:/some/file.ext' => link_to('export:/some/file.ext', export_url_with_ext, :class => 'source download'),
357 'export:/some/file.ext' => link_to('export:/some/file.ext', export_url_with_ext, :class => 'source download'),
358 'export:/some/file@52' => link_to('export:/some/file@52', export_url_with_rev, :class => 'source download'),
358 'export:/some/file@52' => link_to('export:/some/file@52', export_url_with_rev, :class => 'source download'),
359 'export:/some/file.ext@52' => link_to('export:/some/file.ext@52', export_url_with_rev_and_ext, :class => 'source download'),
359 'export:/some/file.ext@52' => link_to('export:/some/file.ext@52', export_url_with_rev_and_ext, :class => 'source download'),
360 'export:/some/file@branch' => link_to('export:/some/file@branch', export_url_with_branch, :class => 'source download'),
360 'export:/some/file@branch' => link_to('export:/some/file@branch', export_url_with_branch, :class => 'source download'),
361 # forum
361 # forum
362 'forum#2' => link_to('Discussion', board_url, :class => 'board'),
362 'forum#2' => link_to('Discussion', board_url, :class => 'board'),
363 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'),
363 'forum:Discussion' => link_to('Discussion', board_url, :class => 'board'),
364 # message
364 # message
365 'message#4' => link_to('Post 2', message_url, :class => 'message'),
365 'message#4' => link_to('Post 2', message_url, :class => 'message'),
366 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
366 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5', :r => 5), :class => 'message'),
367 # news
367 # news
368 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'),
368 'news#1' => link_to('eCookbook first release !', news_url, :class => 'news'),
369 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'),
369 'news:"eCookbook first release !"' => link_to('eCookbook first release !', news_url, :class => 'news'),
370 # project
370 # project
371 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
371 'project#3' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
372 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
372 'project:subproject1' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
373 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
373 'project:"eCookbook subProject 1"' => link_to('eCookbook Subproject 1', project_url, :class => 'project'),
374 # not found
374 # not found
375 '#0123456789' => '#0123456789',
375 '#0123456789' => '#0123456789',
376 # invalid expressions
376 # invalid expressions
377 'source:' => 'source:',
377 'source:' => 'source:',
378 # url hash
378 # url hash
379 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
379 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
380 }
380 }
381 @project = Project.find(1)
381 @project = Project.find(1)
382 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
382 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
383 end
383 end
384
384
385 def test_should_not_parse_redmine_links_inside_link
385 def test_should_not_parse_redmine_links_inside_link
386 raw = "r1 should not be parsed in http://example.com/url-r1/"
386 raw = "r1 should not be parsed in http://example.com/url-r1/"
387 assert_match %r{<p><a class="changeset".*>r1</a> should not be parsed in <a class="external" href="http://example.com/url-r1/">http://example.com/url-r1/</a></p>},
387 assert_match %r{<p><a class="changeset".*>r1</a> should not be parsed in <a class="external" href="http://example.com/url-r1/">http://example.com/url-r1/</a></p>},
388 textilizable(raw, :project => Project.find(1))
388 textilizable(raw, :project => Project.find(1))
389 end
389 end
390
390
391 def test_redmine_links_with_a_different_project_before_current_project
391 def test_redmine_links_with_a_different_project_before_current_project
392 vp1 = Version.generate!(:project_id => 1, :name => '1.4.4')
392 vp1 = Version.generate!(:project_id => 1, :name => '1.4.4')
393 vp3 = Version.generate!(:project_id => 3, :name => '1.4.4')
393 vp3 = Version.generate!(:project_id => 3, :name => '1.4.4')
394 @project = Project.find(3)
394 @project = Project.find(3)
395 result1 = link_to("1.4.4", "/versions/#{vp1.id}", :class => "version")
395 result1 = link_to("1.4.4", "/versions/#{vp1.id}", :class => "version")
396 result2 = link_to("1.4.4", "/versions/#{vp3.id}", :class => "version")
396 result2 = link_to("1.4.4", "/versions/#{vp3.id}", :class => "version")
397 assert_equal "<p>#{result1} #{result2}</p>",
397 assert_equal "<p>#{result1} #{result2}</p>",
398 textilizable("ecookbook:version:1.4.4 version:1.4.4")
398 textilizable("ecookbook:version:1.4.4 version:1.4.4")
399 end
399 end
400
400
401 def test_escaped_redmine_links_should_not_be_parsed
401 def test_escaped_redmine_links_should_not_be_parsed
402 to_test = [
402 to_test = [
403 '#3.',
403 '#3.',
404 '#3-14.',
404 '#3-14.',
405 '#3#-note14.',
405 '#3#-note14.',
406 'r1',
406 'r1',
407 'document#1',
407 'document#1',
408 'document:"Test document"',
408 'document:"Test document"',
409 'version#2',
409 'version#2',
410 'version:1.0',
410 'version:1.0',
411 'version:"1.0"',
411 'version:"1.0"',
412 'source:/some/file'
412 'source:/some/file'
413 ]
413 ]
414 @project = Project.find(1)
414 @project = Project.find(1)
415 to_test.each { |text| assert_equal "<p>#{text}</p>", textilizable("!" + text), "#{text} failed" }
415 to_test.each { |text| assert_equal "<p>#{text}</p>", textilizable("!" + text), "#{text} failed" }
416 end
416 end
417
417
418 def test_cross_project_redmine_links
418 def test_cross_project_redmine_links
419 source_link = link_to('ecookbook:source:/some/file',
419 source_link = link_to('ecookbook:source:/some/file',
420 {:controller => 'repositories', :action => 'entry',
420 {:controller => 'repositories', :action => 'entry',
421 :id => 'ecookbook', :path => ['some', 'file']},
421 :id => 'ecookbook', :path => ['some', 'file']},
422 :class => 'source')
422 :class => 'source')
423 changeset_link = link_to('ecookbook:r2',
423 changeset_link = link_to('ecookbook:r2',
424 {:controller => 'repositories', :action => 'revision',
424 {:controller => 'repositories', :action => 'revision',
425 :id => 'ecookbook', :rev => 2},
425 :id => 'ecookbook', :rev => 2},
426 :class => 'changeset',
426 :class => 'changeset',
427 :title => 'This commit fixes #1, #2 and references #1 & #3')
427 :title => 'This commit fixes #1, #2 and references #1 & #3')
428 to_test = {
428 to_test = {
429 # documents
429 # documents
430 'document:"Test document"' => 'document:"Test document"',
430 'document:"Test document"' => 'document:"Test document"',
431 'ecookbook:document:"Test document"' =>
431 'ecookbook:document:"Test document"' =>
432 link_to("Test document", "/documents/1", :class => "document"),
432 link_to("Test document", "/documents/1", :class => "document"),
433 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
433 'invalid:document:"Test document"' => 'invalid:document:"Test document"',
434 # versions
434 # versions
435 'version:"1.0"' => 'version:"1.0"',
435 'version:"1.0"' => 'version:"1.0"',
436 'ecookbook:version:"1.0"' =>
436 'ecookbook:version:"1.0"' =>
437 link_to("1.0", "/versions/2", :class => "version"),
437 link_to("1.0", "/versions/2", :class => "version"),
438 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
438 'invalid:version:"1.0"' => 'invalid:version:"1.0"',
439 # changeset
439 # changeset
440 'r2' => 'r2',
440 'r2' => 'r2',
441 'ecookbook:r2' => changeset_link,
441 'ecookbook:r2' => changeset_link,
442 'invalid:r2' => 'invalid:r2',
442 'invalid:r2' => 'invalid:r2',
443 # source
443 # source
444 'source:/some/file' => 'source:/some/file',
444 'source:/some/file' => 'source:/some/file',
445 'ecookbook:source:/some/file' => source_link,
445 'ecookbook:source:/some/file' => source_link,
446 'invalid:source:/some/file' => 'invalid:source:/some/file',
446 'invalid:source:/some/file' => 'invalid:source:/some/file',
447 }
447 }
448 @project = Project.find(3)
448 @project = Project.find(3)
449 to_test.each do |text, result|
449 to_test.each do |text, result|
450 assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed"
450 assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed"
451 end
451 end
452 end
452 end
453
453
454 def test_redmine_links_by_name_should_work_with_html_escaped_characters
454 def test_redmine_links_by_name_should_work_with_html_escaped_characters
455 v = Version.generate!(:name => "Test & Show.txt", :project_id => 1)
455 v = Version.generate!(:name => "Test & Show.txt", :project_id => 1)
456 link = link_to("Test & Show.txt", "/versions/#{v.id}", :class => "version")
456 link = link_to("Test & Show.txt", "/versions/#{v.id}", :class => "version")
457
457
458 @project = v.project
458 @project = v.project
459 assert_equal "<p>#{link}</p>", textilizable('version:"Test & Show.txt"')
459 assert_equal "<p>#{link}</p>", textilizable('version:"Test & Show.txt"')
460 end
460 end
461
461
462 def test_link_to_issue_subject
462 def test_link_to_issue_subject
463 issue = Issue.generate!(:subject => "01234567890123456789")
463 issue = Issue.generate!(:subject => "01234567890123456789")
464 str = link_to_issue(issue, :truncate => 10)
464 str = link_to_issue(issue, :truncate => 10)
465 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
465 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
466 assert_equal "#{result}: 0123456...", str
466 assert_equal "#{result}: 0123456...", str
467
467
468 issue = Issue.generate!(:subject => "<&>")
468 issue = Issue.generate!(:subject => "<&>")
469 str = link_to_issue(issue)
469 str = link_to_issue(issue)
470 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
470 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
471 assert_equal "#{result}: &lt;&amp;&gt;", str
471 assert_equal "#{result}: &lt;&amp;&gt;", str
472
472
473 issue = Issue.generate!(:subject => "<&>0123456789012345")
473 issue = Issue.generate!(:subject => "<&>0123456789012345")
474 str = link_to_issue(issue, :truncate => 10)
474 str = link_to_issue(issue, :truncate => 10)
475 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
475 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}", :class => issue.css_classes)
476 assert_equal "#{result}: &lt;&amp;&gt;0123...", str
476 assert_equal "#{result}: &lt;&amp;&gt;0123...", str
477 end
477 end
478
478
479 def test_link_to_issue_title
479 def test_link_to_issue_title
480 long_str = "0123456789" * 5
480 long_str = "0123456789" * 5
481
481
482 issue = Issue.generate!(:subject => "#{long_str}01234567890123456789")
482 issue = Issue.generate!(:subject => "#{long_str}01234567890123456789")
483 str = link_to_issue(issue, :subject => false)
483 str = link_to_issue(issue, :subject => false)
484 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}",
484 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}",
485 :class => issue.css_classes,
485 :class => issue.css_classes,
486 :title => "#{long_str}0123456...")
486 :title => "#{long_str}0123456...")
487 assert_equal result, str
487 assert_equal result, str
488
488
489 issue = Issue.generate!(:subject => "<&>#{long_str}01234567890123456789")
489 issue = Issue.generate!(:subject => "<&>#{long_str}01234567890123456789")
490 str = link_to_issue(issue, :subject => false)
490 str = link_to_issue(issue, :subject => false)
491 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}",
491 result = link_to("Bug ##{issue.id}", "/issues/#{issue.id}",
492 :class => issue.css_classes,
492 :class => issue.css_classes,
493 :title => "<&>#{long_str}0123...")
493 :title => "<&>#{long_str}0123...")
494 assert_equal result, str
494 assert_equal result, str
495 end
495 end
496
496
497 def test_multiple_repositories_redmine_links
497 def test_multiple_repositories_redmine_links
498 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn_repo-1', :url => 'file:///foo/hg')
498 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn_repo-1', :url => 'file:///foo/hg')
499 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
499 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
500 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
500 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
501 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
501 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
502
502
503 changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
503 changeset_link = link_to('r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
504 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
504 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
505 svn_changeset_link = link_to('svn_repo-1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn_repo-1', :rev => 123},
505 svn_changeset_link = link_to('svn_repo-1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn_repo-1', :rev => 123},
506 :class => 'changeset', :title => '')
506 :class => 'changeset', :title => '')
507 hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
507 hg_changeset_link = link_to('hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
508 :class => 'changeset', :title => '')
508 :class => 'changeset', :title => '')
509
509
510 source_link = link_to('source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
510 source_link = link_to('source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
511 hg_source_link = link_to('source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
511 hg_source_link = link_to('source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
512
512
513 to_test = {
513 to_test = {
514 'r2' => changeset_link,
514 'r2' => changeset_link,
515 'svn_repo-1|r123' => svn_changeset_link,
515 'svn_repo-1|r123' => svn_changeset_link,
516 'invalid|r123' => 'invalid|r123',
516 'invalid|r123' => 'invalid|r123',
517 'commit:hg1|abcd' => hg_changeset_link,
517 'commit:hg1|abcd' => hg_changeset_link,
518 'commit:invalid|abcd' => 'commit:invalid|abcd',
518 'commit:invalid|abcd' => 'commit:invalid|abcd',
519 # source
519 # source
520 'source:some/file' => source_link,
520 'source:some/file' => source_link,
521 'source:hg1|some/file' => hg_source_link,
521 'source:hg1|some/file' => hg_source_link,
522 'source:invalid|some/file' => 'source:invalid|some/file',
522 'source:invalid|some/file' => 'source:invalid|some/file',
523 }
523 }
524
524
525 @project = Project.find(1)
525 @project = Project.find(1)
526 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
526 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
527 end
527 end
528
528
529 def test_cross_project_multiple_repositories_redmine_links
529 def test_cross_project_multiple_repositories_redmine_links
530 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
530 svn = Repository::Subversion.create!(:project_id => 1, :identifier => 'svn1', :url => 'file:///foo/hg')
531 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
531 Changeset.create!(:repository => svn, :committed_on => Time.now, :revision => '123')
532 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
532 hg = Repository::Mercurial.create!(:project_id => 1, :identifier => 'hg1', :url => '/foo/hg')
533 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
533 Changeset.create!(:repository => hg, :committed_on => Time.now, :revision => '123', :scmid => 'abcd')
534
534
535 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
535 changeset_link = link_to('ecookbook:r2', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 2},
536 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
536 :class => 'changeset', :title => 'This commit fixes #1, #2 and references #1 & #3')
537 svn_changeset_link = link_to('ecookbook:svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
537 svn_changeset_link = link_to('ecookbook:svn1|r123', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'svn1', :rev => 123},
538 :class => 'changeset', :title => '')
538 :class => 'changeset', :title => '')
539 hg_changeset_link = link_to('ecookbook:hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
539 hg_changeset_link = link_to('ecookbook:hg1|abcd', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :repository_id => 'hg1', :rev => 'abcd'},
540 :class => 'changeset', :title => '')
540 :class => 'changeset', :title => '')
541
541
542 source_link = link_to('ecookbook:source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
542 source_link = link_to('ecookbook:source:some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}, :class => 'source')
543 hg_source_link = link_to('ecookbook:source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
543 hg_source_link = link_to('ecookbook:source:hg1|some/file', {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :repository_id => 'hg1', :path => ['some', 'file']}, :class => 'source')
544
544
545 to_test = {
545 to_test = {
546 'ecookbook:r2' => changeset_link,
546 'ecookbook:r2' => changeset_link,
547 'ecookbook:svn1|r123' => svn_changeset_link,
547 'ecookbook:svn1|r123' => svn_changeset_link,
548 'ecookbook:invalid|r123' => 'ecookbook:invalid|r123',
548 'ecookbook:invalid|r123' => 'ecookbook:invalid|r123',
549 'ecookbook:commit:hg1|abcd' => hg_changeset_link,
549 'ecookbook:commit:hg1|abcd' => hg_changeset_link,
550 'ecookbook:commit:invalid|abcd' => 'ecookbook:commit:invalid|abcd',
550 'ecookbook:commit:invalid|abcd' => 'ecookbook:commit:invalid|abcd',
551 'invalid:commit:invalid|abcd' => 'invalid:commit:invalid|abcd',
551 'invalid:commit:invalid|abcd' => 'invalid:commit:invalid|abcd',
552 # source
552 # source
553 'ecookbook:source:some/file' => source_link,
553 'ecookbook:source:some/file' => source_link,
554 'ecookbook:source:hg1|some/file' => hg_source_link,
554 'ecookbook:source:hg1|some/file' => hg_source_link,
555 'ecookbook:source:invalid|some/file' => 'ecookbook:source:invalid|some/file',
555 'ecookbook:source:invalid|some/file' => 'ecookbook:source:invalid|some/file',
556 'invalid:source:invalid|some/file' => 'invalid:source:invalid|some/file',
556 'invalid:source:invalid|some/file' => 'invalid:source:invalid|some/file',
557 }
557 }
558
558
559 @project = Project.find(3)
559 @project = Project.find(3)
560 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
560 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text), "#{text} failed" }
561 end
561 end
562
562
563 def test_redmine_links_git_commit
563 def test_redmine_links_git_commit
564 changeset_link = link_to('abcd',
564 changeset_link = link_to('abcd',
565 {
565 {
566 :controller => 'repositories',
566 :controller => 'repositories',
567 :action => 'revision',
567 :action => 'revision',
568 :id => 'subproject1',
568 :id => 'subproject1',
569 :rev => 'abcd',
569 :rev => 'abcd',
570 },
570 },
571 :class => 'changeset', :title => 'test commit')
571 :class => 'changeset', :title => 'test commit')
572 to_test = {
572 to_test = {
573 'commit:abcd' => changeset_link,
573 'commit:abcd' => changeset_link,
574 }
574 }
575 @project = Project.find(3)
575 @project = Project.find(3)
576 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
576 r = Repository::Git.create!(:project => @project, :url => '/tmp/test/git')
577 assert r
577 assert r
578 c = Changeset.new(:repository => r,
578 c = Changeset.new(:repository => r,
579 :committed_on => Time.now,
579 :committed_on => Time.now,
580 :revision => 'abcd',
580 :revision => 'abcd',
581 :scmid => 'abcd',
581 :scmid => 'abcd',
582 :comments => 'test commit')
582 :comments => 'test commit')
583 assert( c.save )
583 assert( c.save )
584 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
584 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
585 end
585 end
586
586
587 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
587 # TODO: Bazaar commit id contains mail address, so it contains '@' and '_'.
588 def test_redmine_links_darcs_commit
588 def test_redmine_links_darcs_commit
589 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
589 changeset_link = link_to('20080308225258-98289-abcd456efg.gz',
590 {
590 {
591 :controller => 'repositories',
591 :controller => 'repositories',
592 :action => 'revision',
592 :action => 'revision',
593 :id => 'subproject1',
593 :id => 'subproject1',
594 :rev => '123',
594 :rev => '123',
595 },
595 },
596 :class => 'changeset', :title => 'test commit')
596 :class => 'changeset', :title => 'test commit')
597 to_test = {
597 to_test = {
598 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
598 'commit:20080308225258-98289-abcd456efg.gz' => changeset_link,
599 }
599 }
600 @project = Project.find(3)
600 @project = Project.find(3)
601 r = Repository::Darcs.create!(
601 r = Repository::Darcs.create!(
602 :project => @project, :url => '/tmp/test/darcs',
602 :project => @project, :url => '/tmp/test/darcs',
603 :log_encoding => 'UTF-8')
603 :log_encoding => 'UTF-8')
604 assert r
604 assert r
605 c = Changeset.new(:repository => r,
605 c = Changeset.new(:repository => r,
606 :committed_on => Time.now,
606 :committed_on => Time.now,
607 :revision => '123',
607 :revision => '123',
608 :scmid => '20080308225258-98289-abcd456efg.gz',
608 :scmid => '20080308225258-98289-abcd456efg.gz',
609 :comments => 'test commit')
609 :comments => 'test commit')
610 assert( c.save )
610 assert( c.save )
611 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
611 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
612 end
612 end
613
613
614 def test_redmine_links_mercurial_commit
614 def test_redmine_links_mercurial_commit
615 changeset_link_rev = link_to('r123',
615 changeset_link_rev = link_to('r123',
616 {
616 {
617 :controller => 'repositories',
617 :controller => 'repositories',
618 :action => 'revision',
618 :action => 'revision',
619 :id => 'subproject1',
619 :id => 'subproject1',
620 :rev => '123' ,
620 :rev => '123' ,
621 },
621 },
622 :class => 'changeset', :title => 'test commit')
622 :class => 'changeset', :title => 'test commit')
623 changeset_link_commit = link_to('abcd',
623 changeset_link_commit = link_to('abcd',
624 {
624 {
625 :controller => 'repositories',
625 :controller => 'repositories',
626 :action => 'revision',
626 :action => 'revision',
627 :id => 'subproject1',
627 :id => 'subproject1',
628 :rev => 'abcd' ,
628 :rev => 'abcd' ,
629 },
629 },
630 :class => 'changeset', :title => 'test commit')
630 :class => 'changeset', :title => 'test commit')
631 to_test = {
631 to_test = {
632 'r123' => changeset_link_rev,
632 'r123' => changeset_link_rev,
633 'commit:abcd' => changeset_link_commit,
633 'commit:abcd' => changeset_link_commit,
634 }
634 }
635 @project = Project.find(3)
635 @project = Project.find(3)
636 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
636 r = Repository::Mercurial.create!(:project => @project, :url => '/tmp/test')
637 assert r
637 assert r
638 c = Changeset.new(:repository => r,
638 c = Changeset.new(:repository => r,
639 :committed_on => Time.now,
639 :committed_on => Time.now,
640 :revision => '123',
640 :revision => '123',
641 :scmid => 'abcd',
641 :scmid => 'abcd',
642 :comments => 'test commit')
642 :comments => 'test commit')
643 assert( c.save )
643 assert( c.save )
644 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
644 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
645 end
645 end
646
646
647 def test_attachment_links
647 def test_attachment_links
648 text = 'attachment:error281.txt'
648 text = 'attachment:error281.txt'
649 result = link_to("error281.txt", "/attachments/download/1/error281.txt",
649 result = link_to("error281.txt", "/attachments/download/1/error281.txt",
650 :class => "attachment")
650 :class => "attachment")
651 assert_equal "<p>#{result}</p>",
651 assert_equal "<p>#{result}</p>",
652 textilizable(text,
652 textilizable(text,
653 :attachments => Issue.find(3).attachments),
653 :attachments => Issue.find(3).attachments),
654 "#{text} failed"
654 "#{text} failed"
655 end
655 end
656
656
657 def test_attachment_link_should_link_to_latest_attachment
657 def test_attachment_link_should_link_to_latest_attachment
658 set_tmp_attachments_directory
658 set_tmp_attachments_directory
659 a1 = Attachment.generate!(:filename => "test.txt", :created_on => 1.hour.ago)
659 a1 = Attachment.generate!(:filename => "test.txt", :created_on => 1.hour.ago)
660 a2 = Attachment.generate!(:filename => "test.txt")
660 a2 = Attachment.generate!(:filename => "test.txt")
661 result = link_to("test.txt", "/attachments/download/#{a2.id}/test.txt",
661 result = link_to("test.txt", "/attachments/download/#{a2.id}/test.txt",
662 :class => "attachment")
662 :class => "attachment")
663 assert_equal "<p>#{result}</p>",
663 assert_equal "<p>#{result}</p>",
664 textilizable('attachment:test.txt', :attachments => [a1, a2])
664 textilizable('attachment:test.txt', :attachments => [a1, a2])
665 end
665 end
666
666
667 def test_wiki_links
667 def test_wiki_links
668 russian_eacape = CGI.escape(@russian_test)
668 russian_eacape = CGI.escape(@russian_test)
669 to_test = {
669 to_test = {
670 '[[CookBook documentation]]' =>
670 '[[CookBook documentation]]' =>
671 link_to("CookBook documentation",
671 link_to("CookBook documentation",
672 "/projects/ecookbook/wiki/CookBook_documentation",
672 "/projects/ecookbook/wiki/CookBook_documentation",
673 :class => "wiki-page"),
673 :class => "wiki-page"),
674 '[[Another page|Page]]' =>
674 '[[Another page|Page]]' =>
675 link_to("Page",
675 link_to("Page",
676 "/projects/ecookbook/wiki/Another_page",
676 "/projects/ecookbook/wiki/Another_page",
677 :class => "wiki-page"),
677 :class => "wiki-page"),
678 # title content should be formatted
678 # title content should be formatted
679 '[[Another page|With _styled_ *title*]]' =>
679 '[[Another page|With _styled_ *title*]]' =>
680 link_to("With <em>styled</em> <strong>title</strong>".html_safe,
680 link_to("With <em>styled</em> <strong>title</strong>".html_safe,
681 "/projects/ecookbook/wiki/Another_page",
681 "/projects/ecookbook/wiki/Another_page",
682 :class => "wiki-page"),
682 :class => "wiki-page"),
683 '[[Another page|With title containing <strong>HTML entities &amp; markups</strong>]]' =>
683 '[[Another page|With title containing <strong>HTML entities &amp; markups</strong>]]' =>
684 link_to("With title containing &lt;strong&gt;HTML entities &amp; markups&lt;/strong&gt;".html_safe,
684 link_to("With title containing &lt;strong&gt;HTML entities &amp; markups&lt;/strong&gt;".html_safe,
685 "/projects/ecookbook/wiki/Another_page",
685 "/projects/ecookbook/wiki/Another_page",
686 :class => "wiki-page"),
686 :class => "wiki-page"),
687 # link with anchor
687 # link with anchor
688 '[[CookBook documentation#One-section]]' =>
688 '[[CookBook documentation#One-section]]' =>
689 link_to("CookBook documentation",
689 link_to("CookBook documentation",
690 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
690 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
691 :class => "wiki-page"),
691 :class => "wiki-page"),
692 '[[Another page#anchor|Page]]' =>
692 '[[Another page#anchor|Page]]' =>
693 link_to("Page",
693 link_to("Page",
694 "/projects/ecookbook/wiki/Another_page#anchor",
694 "/projects/ecookbook/wiki/Another_page#anchor",
695 :class => "wiki-page"),
695 :class => "wiki-page"),
696 # UTF8 anchor
696 # UTF8 anchor
697 "[[Another_page##{@russian_test}|#{@russian_test}]]" =>
697 "[[Another_page##{@russian_test}|#{@russian_test}]]" =>
698 link_to(@russian_test,
698 link_to(@russian_test,
699 "/projects/ecookbook/wiki/Another_page##{russian_eacape}",
699 "/projects/ecookbook/wiki/Another_page##{russian_eacape}",
700 :class => "wiki-page"),
700 :class => "wiki-page"),
701 # page that doesn't exist
701 # page that doesn't exist
702 '[[Unknown page]]' =>
702 '[[Unknown page]]' =>
703 link_to("Unknown page",
703 link_to("Unknown page",
704 "/projects/ecookbook/wiki/Unknown_page",
704 "/projects/ecookbook/wiki/Unknown_page",
705 :class => "wiki-page new"),
705 :class => "wiki-page new"),
706 '[[Unknown page|404]]' =>
706 '[[Unknown page|404]]' =>
707 link_to("404",
707 link_to("404",
708 "/projects/ecookbook/wiki/Unknown_page",
708 "/projects/ecookbook/wiki/Unknown_page",
709 :class => "wiki-page new"),
709 :class => "wiki-page new"),
710 # link to another project wiki
710 # link to another project wiki
711 '[[onlinestore:]]' =>
711 '[[onlinestore:]]' =>
712 link_to("onlinestore",
712 link_to("onlinestore",
713 "/projects/onlinestore/wiki",
713 "/projects/onlinestore/wiki",
714 :class => "wiki-page"),
714 :class => "wiki-page"),
715 '[[onlinestore:|Wiki]]' =>
715 '[[onlinestore:|Wiki]]' =>
716 link_to("Wiki",
716 link_to("Wiki",
717 "/projects/onlinestore/wiki",
717 "/projects/onlinestore/wiki",
718 :class => "wiki-page"),
718 :class => "wiki-page"),
719 '[[onlinestore:Start page]]' =>
719 '[[onlinestore:Start page]]' =>
720 link_to("Start page",
720 link_to("Start page",
721 "/projects/onlinestore/wiki/Start_page",
721 "/projects/onlinestore/wiki/Start_page",
722 :class => "wiki-page"),
722 :class => "wiki-page"),
723 '[[onlinestore:Start page|Text]]' =>
723 '[[onlinestore:Start page|Text]]' =>
724 link_to("Text",
724 link_to("Text",
725 "/projects/onlinestore/wiki/Start_page",
725 "/projects/onlinestore/wiki/Start_page",
726 :class => "wiki-page"),
726 :class => "wiki-page"),
727 '[[onlinestore:Unknown page]]' =>
727 '[[onlinestore:Unknown page]]' =>
728 link_to("Unknown page",
728 link_to("Unknown page",
729 "/projects/onlinestore/wiki/Unknown_page",
729 "/projects/onlinestore/wiki/Unknown_page",
730 :class => "wiki-page new"),
730 :class => "wiki-page new"),
731 # struck through link
731 # struck through link
732 '-[[Another page|Page]]-' =>
732 '-[[Another page|Page]]-' =>
733 "<del>".html_safe +
733 "<del>".html_safe +
734 link_to("Page",
734 link_to("Page",
735 "/projects/ecookbook/wiki/Another_page",
735 "/projects/ecookbook/wiki/Another_page",
736 :class => "wiki-page").html_safe +
736 :class => "wiki-page").html_safe +
737 "</del>".html_safe,
737 "</del>".html_safe,
738 '-[[Another page|Page]] link-' =>
738 '-[[Another page|Page]] link-' =>
739 "<del>".html_safe +
739 "<del>".html_safe +
740 link_to("Page",
740 link_to("Page",
741 "/projects/ecookbook/wiki/Another_page",
741 "/projects/ecookbook/wiki/Another_page",
742 :class => "wiki-page").html_safe +
742 :class => "wiki-page").html_safe +
743 " link</del>".html_safe,
743 " link</del>".html_safe,
744 # escaping
744 # escaping
745 '![[Another page|Page]]' => '[[Another page|Page]]',
745 '![[Another page|Page]]' => '[[Another page|Page]]',
746 # project does not exist
746 # project does not exist
747 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
747 '[[unknowproject:Start]]' => '[[unknowproject:Start]]',
748 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
748 '[[unknowproject:Start|Page title]]' => '[[unknowproject:Start|Page title]]',
749 }
749 }
750 @project = Project.find(1)
750 @project = Project.find(1)
751 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
751 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
752 end
752 end
753
753
754 def test_wiki_links_within_local_file_generation_context
754 def test_wiki_links_within_local_file_generation_context
755 to_test = {
755 to_test = {
756 # link to a page
756 # link to a page
757 '[[CookBook documentation]]' =>
757 '[[CookBook documentation]]' =>
758 link_to("CookBook documentation", "CookBook_documentation.html",
758 link_to("CookBook documentation", "CookBook_documentation.html",
759 :class => "wiki-page"),
759 :class => "wiki-page"),
760 '[[CookBook documentation|documentation]]' =>
760 '[[CookBook documentation|documentation]]' =>
761 link_to("documentation", "CookBook_documentation.html",
761 link_to("documentation", "CookBook_documentation.html",
762 :class => "wiki-page"),
762 :class => "wiki-page"),
763 '[[CookBook documentation#One-section]]' =>
763 '[[CookBook documentation#One-section]]' =>
764 link_to("CookBook documentation", "CookBook_documentation.html#One-section",
764 link_to("CookBook documentation", "CookBook_documentation.html#One-section",
765 :class => "wiki-page"),
765 :class => "wiki-page"),
766 '[[CookBook documentation#One-section|documentation]]' =>
766 '[[CookBook documentation#One-section|documentation]]' =>
767 link_to("documentation", "CookBook_documentation.html#One-section",
767 link_to("documentation", "CookBook_documentation.html#One-section",
768 :class => "wiki-page"),
768 :class => "wiki-page"),
769 # page that doesn't exist
769 # page that doesn't exist
770 '[[Unknown page]]' =>
770 '[[Unknown page]]' =>
771 link_to("Unknown page", "Unknown_page.html",
771 link_to("Unknown page", "Unknown_page.html",
772 :class => "wiki-page new"),
772 :class => "wiki-page new"),
773 '[[Unknown page|404]]' =>
773 '[[Unknown page|404]]' =>
774 link_to("404", "Unknown_page.html",
774 link_to("404", "Unknown_page.html",
775 :class => "wiki-page new"),
775 :class => "wiki-page new"),
776 '[[Unknown page#anchor]]' =>
776 '[[Unknown page#anchor]]' =>
777 link_to("Unknown page", "Unknown_page.html#anchor",
777 link_to("Unknown page", "Unknown_page.html#anchor",
778 :class => "wiki-page new"),
778 :class => "wiki-page new"),
779 '[[Unknown page#anchor|404]]' =>
779 '[[Unknown page#anchor|404]]' =>
780 link_to("404", "Unknown_page.html#anchor",
780 link_to("404", "Unknown_page.html#anchor",
781 :class => "wiki-page new"),
781 :class => "wiki-page new"),
782 }
782 }
783 @project = Project.find(1)
783 @project = Project.find(1)
784 to_test.each do |text, result|
784 to_test.each do |text, result|
785 assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local)
785 assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :local)
786 end
786 end
787 end
787 end
788
788
789 def test_wiki_links_within_wiki_page_context
789 def test_wiki_links_within_wiki_page_context
790 page = WikiPage.find_by_title('Another_page' )
790 page = WikiPage.find_by_title('Another_page' )
791 to_test = {
791 to_test = {
792 '[[CookBook documentation]]' =>
792 '[[CookBook documentation]]' =>
793 link_to("CookBook documentation",
793 link_to("CookBook documentation",
794 "/projects/ecookbook/wiki/CookBook_documentation",
794 "/projects/ecookbook/wiki/CookBook_documentation",
795 :class => "wiki-page"),
795 :class => "wiki-page"),
796 '[[CookBook documentation|documentation]]' =>
796 '[[CookBook documentation|documentation]]' =>
797 link_to("documentation",
797 link_to("documentation",
798 "/projects/ecookbook/wiki/CookBook_documentation",
798 "/projects/ecookbook/wiki/CookBook_documentation",
799 :class => "wiki-page"),
799 :class => "wiki-page"),
800 '[[CookBook documentation#One-section]]' =>
800 '[[CookBook documentation#One-section]]' =>
801 link_to("CookBook documentation",
801 link_to("CookBook documentation",
802 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
802 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
803 :class => "wiki-page"),
803 :class => "wiki-page"),
804 '[[CookBook documentation#One-section|documentation]]' =>
804 '[[CookBook documentation#One-section|documentation]]' =>
805 link_to("documentation",
805 link_to("documentation",
806 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
806 "/projects/ecookbook/wiki/CookBook_documentation#One-section",
807 :class => "wiki-page"),
807 :class => "wiki-page"),
808 # link to the current page
808 # link to the current page
809 '[[Another page]]' =>
809 '[[Another page]]' =>
810 link_to("Another page",
810 link_to("Another page",
811 "/projects/ecookbook/wiki/Another_page",
811 "/projects/ecookbook/wiki/Another_page",
812 :class => "wiki-page"),
812 :class => "wiki-page"),
813 '[[Another page|Page]]' =>
813 '[[Another page|Page]]' =>
814 link_to("Page",
814 link_to("Page",
815 "/projects/ecookbook/wiki/Another_page",
815 "/projects/ecookbook/wiki/Another_page",
816 :class => "wiki-page"),
816 :class => "wiki-page"),
817 '[[Another page#anchor]]' =>
817 '[[Another page#anchor]]' =>
818 link_to("Another page",
818 link_to("Another page",
819 "#anchor",
819 "#anchor",
820 :class => "wiki-page"),
820 :class => "wiki-page"),
821 '[[Another page#anchor|Page]]' =>
821 '[[Another page#anchor|Page]]' =>
822 link_to("Page",
822 link_to("Page",
823 "#anchor",
823 "#anchor",
824 :class => "wiki-page"),
824 :class => "wiki-page"),
825 # page that doesn't exist
825 # page that doesn't exist
826 '[[Unknown page]]' =>
826 '[[Unknown page]]' =>
827 link_to("Unknown page",
827 link_to("Unknown page",
828 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page",
828 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page",
829 :class => "wiki-page new"),
829 :class => "wiki-page new"),
830 '[[Unknown page|404]]' =>
830 '[[Unknown page|404]]' =>
831 link_to("404",
831 link_to("404",
832 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page",
832 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page",
833 :class => "wiki-page new"),
833 :class => "wiki-page new"),
834 '[[Unknown page#anchor]]' =>
834 '[[Unknown page#anchor]]' =>
835 link_to("Unknown page",
835 link_to("Unknown page",
836 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor",
836 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor",
837 :class => "wiki-page new"),
837 :class => "wiki-page new"),
838 '[[Unknown page#anchor|404]]' =>
838 '[[Unknown page#anchor|404]]' =>
839 link_to("404",
839 link_to("404",
840 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor",
840 "/projects/ecookbook/wiki/Unknown_page?parent=Another_page#anchor",
841 :class => "wiki-page new"),
841 :class => "wiki-page new"),
842 }
842 }
843 @project = Project.find(1)
843 @project = Project.find(1)
844 to_test.each do |text, result|
844 to_test.each do |text, result|
845 assert_equal "<p>#{result}</p>",
845 assert_equal "<p>#{result}</p>",
846 textilizable(WikiContent.new( :text => text, :page => page ), :text)
846 textilizable(WikiContent.new( :text => text, :page => page ), :text)
847 end
847 end
848 end
848 end
849
849
850 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
850 def test_wiki_links_anchor_option_should_prepend_page_title_to_href
851 to_test = {
851 to_test = {
852 # link to a page
852 # link to a page
853 '[[CookBook documentation]]' =>
853 '[[CookBook documentation]]' =>
854 link_to("CookBook documentation",
854 link_to("CookBook documentation",
855 "#CookBook_documentation",
855 "#CookBook_documentation",
856 :class => "wiki-page"),
856 :class => "wiki-page"),
857 '[[CookBook documentation|documentation]]' =>
857 '[[CookBook documentation|documentation]]' =>
858 link_to("documentation",
858 link_to("documentation",
859 "#CookBook_documentation",
859 "#CookBook_documentation",
860 :class => "wiki-page"),
860 :class => "wiki-page"),
861 '[[CookBook documentation#One-section]]' =>
861 '[[CookBook documentation#One-section]]' =>
862 link_to("CookBook documentation",
862 link_to("CookBook documentation",
863 "#CookBook_documentation_One-section",
863 "#CookBook_documentation_One-section",
864 :class => "wiki-page"),
864 :class => "wiki-page"),
865 '[[CookBook documentation#One-section|documentation]]' =>
865 '[[CookBook documentation#One-section|documentation]]' =>
866 link_to("documentation",
866 link_to("documentation",
867 "#CookBook_documentation_One-section",
867 "#CookBook_documentation_One-section",
868 :class => "wiki-page"),
868 :class => "wiki-page"),
869 # page that doesn't exist
869 # page that doesn't exist
870 '[[Unknown page]]' =>
870 '[[Unknown page]]' =>
871 link_to("Unknown page",
871 link_to("Unknown page",
872 "#Unknown_page",
872 "#Unknown_page",
873 :class => "wiki-page new"),
873 :class => "wiki-page new"),
874 '[[Unknown page|404]]' =>
874 '[[Unknown page|404]]' =>
875 link_to("404",
875 link_to("404",
876 "#Unknown_page",
876 "#Unknown_page",
877 :class => "wiki-page new"),
877 :class => "wiki-page new"),
878 '[[Unknown page#anchor]]' =>
878 '[[Unknown page#anchor]]' =>
879 link_to("Unknown page",
879 link_to("Unknown page",
880 "#Unknown_page_anchor",
880 "#Unknown_page_anchor",
881 :class => "wiki-page new"),
881 :class => "wiki-page new"),
882 '[[Unknown page#anchor|404]]' =>
882 '[[Unknown page#anchor|404]]' =>
883 link_to("404",
883 link_to("404",
884 "#Unknown_page_anchor",
884 "#Unknown_page_anchor",
885 :class => "wiki-page new"),
885 :class => "wiki-page new"),
886 }
886 }
887 @project = Project.find(1)
887 @project = Project.find(1)
888 to_test.each do |text, result|
888 to_test.each do |text, result|
889 assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor)
889 assert_equal "<p>#{result}</p>", textilizable(text, :wiki_links => :anchor)
890 end
890 end
891 end
891 end
892
892
893 def test_html_tags
893 def test_html_tags
894 to_test = {
894 to_test = {
895 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
895 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
896 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
896 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
897 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
897 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
898 # do not escape pre/code tags
898 # do not escape pre/code tags
899 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
899 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
900 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
900 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
901 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
901 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
902 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
902 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
903 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
903 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
904 # remove attributes except class
904 # remove attributes except class
905 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
905 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
906 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
906 '<pre class="foo">some text</pre>' => '<pre class="foo">some text</pre>',
907 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
907 "<pre class='foo bar'>some text</pre>" => "<pre class='foo bar'>some text</pre>",
908 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
908 '<pre class="foo bar">some text</pre>' => '<pre class="foo bar">some text</pre>',
909 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
909 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
910 # xss
910 # xss
911 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
911 '<pre><code class=""onmouseover="alert(1)">text</code></pre>' => '<pre><code>text</code></pre>',
912 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
912 '<pre class=""onmouseover="alert(1)">text</pre>' => '<pre>text</pre>',
913 }
913 }
914 to_test.each { |text, result| assert_equal result, textilizable(text) }
914 to_test.each { |text, result| assert_equal result, textilizable(text) }
915 end
915 end
916
916
917 def test_allowed_html_tags
917 def test_allowed_html_tags
918 to_test = {
918 to_test = {
919 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
919 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
920 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
920 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
921 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
921 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
922 }
922 }
923 to_test.each { |text, result| assert_equal result, textilizable(text) }
923 to_test.each { |text, result| assert_equal result, textilizable(text) }
924 end
924 end
925
925
926 def test_pre_tags
926 def test_pre_tags
927 raw = <<-RAW
927 raw = <<-RAW
928 Before
928 Before
929
929
930 <pre>
930 <pre>
931 <prepared-statement-cache-size>32</prepared-statement-cache-size>
931 <prepared-statement-cache-size>32</prepared-statement-cache-size>
932 </pre>
932 </pre>
933
933
934 After
934 After
935 RAW
935 RAW
936
936
937 expected = <<-EXPECTED
937 expected = <<-EXPECTED
938 <p>Before</p>
938 <p>Before</p>
939 <pre>
939 <pre>
940 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
940 &lt;prepared-statement-cache-size&gt;32&lt;/prepared-statement-cache-size&gt;
941 </pre>
941 </pre>
942 <p>After</p>
942 <p>After</p>
943 EXPECTED
943 EXPECTED
944
944
945 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
945 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
946 end
946 end
947
947
948 def test_pre_content_should_not_parse_wiki_and_redmine_links
948 def test_pre_content_should_not_parse_wiki_and_redmine_links
949 raw = <<-RAW
949 raw = <<-RAW
950 [[CookBook documentation]]
950 [[CookBook documentation]]
951
951
952 #1
952 #1
953
953
954 <pre>
954 <pre>
955 [[CookBook documentation]]
955 [[CookBook documentation]]
956
956
957 #1
957 #1
958 </pre>
958 </pre>
959 RAW
959 RAW
960
960
961 result1 = link_to("CookBook documentation",
961 result1 = link_to("CookBook documentation",
962 "/projects/ecookbook/wiki/CookBook_documentation",
962 "/projects/ecookbook/wiki/CookBook_documentation",
963 :class => "wiki-page")
963 :class => "wiki-page")
964 result2 = link_to('#1',
964 result2 = link_to('#1',
965 "/issues/1",
965 "/issues/1",
966 :class => Issue.find(1).css_classes,
966 :class => Issue.find(1).css_classes,
967 :title => "Bug: Cannot print recipes (New)")
967 :title => "Bug: Cannot print recipes (New)")
968
968
969 expected = <<-EXPECTED
969 expected = <<-EXPECTED
970 <p>#{result1}</p>
970 <p>#{result1}</p>
971 <p>#{result2}</p>
971 <p>#{result2}</p>
972 <pre>
972 <pre>
973 [[CookBook documentation]]
973 [[CookBook documentation]]
974
974
975 #1
975 #1
976 </pre>
976 </pre>
977 EXPECTED
977 EXPECTED
978
978
979 @project = Project.find(1)
979 @project = Project.find(1)
980 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
980 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
981 end
981 end
982
982
983 def test_non_closing_pre_blocks_should_be_closed
983 def test_non_closing_pre_blocks_should_be_closed
984 raw = <<-RAW
984 raw = <<-RAW
985 <pre><code>
985 <pre><code>
986 RAW
986 RAW
987
987
988 expected = <<-EXPECTED
988 expected = <<-EXPECTED
989 <pre><code>
989 <pre><code>
990 </code></pre>
990 </code></pre>
991 EXPECTED
991 EXPECTED
992
992
993 @project = Project.find(1)
993 @project = Project.find(1)
994 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
994 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
995 end
995 end
996
996
997 def test_unbalanced_closing_pre_tag_should_not_error
997 def test_unbalanced_closing_pre_tag_should_not_error
998 assert_nothing_raised do
998 assert_nothing_raised do
999 textilizable("unbalanced</pre>")
999 textilizable("unbalanced</pre>")
1000 end
1000 end
1001 end
1001 end
1002
1002
1003 def test_syntax_highlight
1003 def test_syntax_highlight
1004 raw = <<-RAW
1004 raw = <<-RAW
1005 <pre><code class="ruby">
1005 <pre><code class="ruby">
1006 # Some ruby code here
1006 # Some ruby code here
1007 </code></pre>
1007 </code></pre>
1008 RAW
1008 RAW
1009
1009
1010 expected = <<-EXPECTED
1010 expected = <<-EXPECTED
1011 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="comment"># Some ruby code here</span></span>
1011 <pre><code class="ruby syntaxhl"><span class=\"CodeRay\"><span class="comment"># Some ruby code here</span></span>
1012 </code></pre>
1012 </code></pre>
1013 EXPECTED
1013 EXPECTED
1014
1014
1015 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
1015 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
1016 end
1016 end
1017
1017
1018 def test_to_path_param
1018 def test_to_path_param
1019 assert_equal 'test1/test2', to_path_param('test1/test2')
1019 assert_equal 'test1/test2', to_path_param('test1/test2')
1020 assert_equal 'test1/test2', to_path_param('/test1/test2/')
1020 assert_equal 'test1/test2', to_path_param('/test1/test2/')
1021 assert_equal 'test1/test2', to_path_param('//test1/test2/')
1021 assert_equal 'test1/test2', to_path_param('//test1/test2/')
1022 assert_equal nil, to_path_param('/')
1022 assert_equal nil, to_path_param('/')
1023 end
1023 end
1024
1024
1025 def test_wiki_links_in_tables
1025 def test_wiki_links_in_tables
1026 text = "|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|"
1026 text = "|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|"
1027 link1 = link_to("Link title", "/projects/ecookbook/wiki/Page", :class => "wiki-page new")
1027 link1 = link_to("Link title", "/projects/ecookbook/wiki/Page", :class => "wiki-page new")
1028 link2 = link_to("Other title", "/projects/ecookbook/wiki/Other_Page", :class => "wiki-page new")
1028 link2 = link_to("Other title", "/projects/ecookbook/wiki/Other_Page", :class => "wiki-page new")
1029 link3 = link_to("Last page", "/projects/ecookbook/wiki/Last_page", :class => "wiki-page new")
1029 link3 = link_to("Last page", "/projects/ecookbook/wiki/Last_page", :class => "wiki-page new")
1030 result = "<tr><td>#{link1}</td>" +
1030 result = "<tr><td>#{link1}</td>" +
1031 "<td>#{link2}</td>" +
1031 "<td>#{link2}</td>" +
1032 "</tr><tr><td>Cell 21</td><td>#{link3}</td></tr>"
1032 "</tr><tr><td>Cell 21</td><td>#{link3}</td></tr>"
1033 @project = Project.find(1)
1033 @project = Project.find(1)
1034 assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '')
1034 assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '')
1035 end
1035 end
1036
1036
1037 def test_text_formatting
1037 def test_text_formatting
1038 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
1038 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
1039 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
1039 '(_text within parentheses_)' => '(<em>text within parentheses</em>)',
1040 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
1040 'a *Humane Web* Text Generator' => 'a <strong>Humane Web</strong> Text Generator',
1041 'a H *umane* W *eb* T *ext* G *enerator*' => 'a H <strong>umane</strong> W <strong>eb</strong> T <strong>ext</strong> G <strong>enerator</strong>',
1041 'a H *umane* W *eb* T *ext* G *enerator*' => 'a H <strong>umane</strong> W <strong>eb</strong> T <strong>ext</strong> G <strong>enerator</strong>',
1042 'a *H* umane *W* eb *T* ext *G* enerator' => 'a <strong>H</strong> umane <strong>W</strong> eb <strong>T</strong> ext <strong>G</strong> enerator',
1042 'a *H* umane *W* eb *T* ext *G* enerator' => 'a <strong>H</strong> umane <strong>W</strong> eb <strong>T</strong> ext <strong>G</strong> enerator',
1043 }
1043 }
1044 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
1044 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
1045 end
1045 end
1046
1046
1047 def test_wiki_horizontal_rule
1047 def test_wiki_horizontal_rule
1048 assert_equal '<hr />', textilizable('---')
1048 assert_equal '<hr />', textilizable('---')
1049 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
1049 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
1050 end
1050 end
1051
1051
1052 def test_footnotes
1052 def test_footnotes
1053 raw = <<-RAW
1053 raw = <<-RAW
1054 This is some text[1].
1054 This is some text[1].
1055
1055
1056 fn1. This is the foot note
1056 fn1. This is the foot note
1057 RAW
1057 RAW
1058
1058
1059 expected = <<-EXPECTED
1059 expected = <<-EXPECTED
1060 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
1060 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
1061 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
1061 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
1062 EXPECTED
1062 EXPECTED
1063
1063
1064 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
1064 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
1065 end
1065 end
1066
1066
1067 def test_headings
1067 def test_headings
1068 raw = 'h1. Some heading'
1068 raw = 'h1. Some heading'
1069 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
1069 expected = %|<a name="Some-heading"></a>\n<h1 >Some heading<a href="#Some-heading" class="wiki-anchor">&para;</a></h1>|
1070
1070
1071 assert_equal expected, textilizable(raw)
1071 assert_equal expected, textilizable(raw)
1072 end
1072 end
1073
1073
1074 def test_headings_with_special_chars
1074 def test_headings_with_special_chars
1075 # This test makes sure that the generated anchor names match the expected
1075 # This test makes sure that the generated anchor names match the expected
1076 # ones even if the heading text contains unconventional characters
1076 # ones even if the heading text contains unconventional characters
1077 raw = 'h1. Some heading related to version 0.5'
1077 raw = 'h1. Some heading related to version 0.5'
1078 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
1078 anchor = sanitize_anchor_name("Some-heading-related-to-version-0.5")
1079 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
1079 expected = %|<a name="#{anchor}"></a>\n<h1 >Some heading related to version 0.5<a href="##{anchor}" class="wiki-anchor">&para;</a></h1>|
1080
1080
1081 assert_equal expected, textilizable(raw)
1081 assert_equal expected, textilizable(raw)
1082 end
1082 end
1083
1083
1084 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
1084 def test_headings_in_wiki_single_page_export_should_be_prepended_with_page_title
1085 page = WikiPage.new( :title => 'Page Title', :wiki_id => 1 )
1085 page = WikiPage.new( :title => 'Page Title', :wiki_id => 1 )
1086 content = WikiContent.new( :text => 'h1. Some heading', :page => page )
1086 content = WikiContent.new( :text => 'h1. Some heading', :page => page )
1087
1087
1088 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
1088 expected = %|<a name="Page_Title_Some-heading"></a>\n<h1 >Some heading<a href="#Page_Title_Some-heading" class="wiki-anchor">&para;</a></h1>|
1089
1089
1090 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
1090 assert_equal expected, textilizable(content, :text, :wiki_links => :anchor )
1091 end
1091 end
1092
1092
1093 def test_table_of_content
1093 def test_table_of_content
1094 raw = <<-RAW
1094 raw = <<-RAW
1095 {{toc}}
1095 {{toc}}
1096
1096
1097 h1. Title
1097 h1. Title
1098
1098
1099 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1099 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1100
1100
1101 h2. Subtitle with a [[Wiki]] link
1101 h2. Subtitle with a [[Wiki]] link
1102
1102
1103 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
1103 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
1104
1104
1105 h2. Subtitle with [[Wiki|another Wiki]] link
1105 h2. Subtitle with [[Wiki|another Wiki]] link
1106
1106
1107 h2. Subtitle with %{color:red}red text%
1107 h2. Subtitle with %{color:red}red text%
1108
1108
1109 <pre>
1109 <pre>
1110 some code
1110 some code
1111 </pre>
1111 </pre>
1112
1112
1113 h3. Subtitle with *some* _modifiers_
1113 h3. Subtitle with *some* _modifiers_
1114
1114
1115 h3. Subtitle with @inline code@
1115 h3. Subtitle with @inline code@
1116
1116
1117 h1. Another title
1117 h1. Another title
1118
1118
1119 h3. An "Internet link":http://www.redmine.org/ inside subtitle
1119 h3. An "Internet link":http://www.redmine.org/ inside subtitle
1120
1120
1121 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
1121 h2. "Project Name !/attachments/1234/logo_small.gif! !/attachments/5678/logo_2.png!":/projects/projectname/issues
1122
1122
1123 RAW
1123 RAW
1124
1124
1125 expected = '<ul class="toc">' +
1125 expected = '<ul class="toc">' +
1126 '<li><a href="#Title">Title</a>' +
1126 '<li><a href="#Title">Title</a>' +
1127 '<ul>' +
1127 '<ul>' +
1128 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
1128 '<li><a href="#Subtitle-with-a-Wiki-link">Subtitle with a Wiki link</a></li>' +
1129 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
1129 '<li><a href="#Subtitle-with-another-Wiki-link">Subtitle with another Wiki link</a></li>' +
1130 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
1130 '<li><a href="#Subtitle-with-red-text">Subtitle with red text</a>' +
1131 '<ul>' +
1131 '<ul>' +
1132 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
1132 '<li><a href="#Subtitle-with-some-modifiers">Subtitle with some modifiers</a></li>' +
1133 '<li><a href="#Subtitle-with-inline-code">Subtitle with inline code</a></li>' +
1133 '<li><a href="#Subtitle-with-inline-code">Subtitle with inline code</a></li>' +
1134 '</ul>' +
1134 '</ul>' +
1135 '</li>' +
1135 '</li>' +
1136 '</ul>' +
1136 '</ul>' +
1137 '</li>' +
1137 '</li>' +
1138 '<li><a href="#Another-title">Another title</a>' +
1138 '<li><a href="#Another-title">Another title</a>' +
1139 '<ul>' +
1139 '<ul>' +
1140 '<li>' +
1140 '<li>' +
1141 '<ul>' +
1141 '<ul>' +
1142 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
1142 '<li><a href="#An-Internet-link-inside-subtitle">An Internet link inside subtitle</a></li>' +
1143 '</ul>' +
1143 '</ul>' +
1144 '</li>' +
1144 '</li>' +
1145 '<li><a href="#Project-Name">Project Name</a></li>' +
1145 '<li><a href="#Project-Name">Project Name</a></li>' +
1146 '</ul>' +
1146 '</ul>' +
1147 '</li>' +
1147 '</li>' +
1148 '</ul>'
1148 '</ul>'
1149
1149
1150 @project = Project.find(1)
1150 @project = Project.find(1)
1151 assert textilizable(raw).gsub("\n", "").include?(expected)
1151 assert textilizable(raw).gsub("\n", "").include?(expected)
1152 end
1152 end
1153
1153
1154 def test_table_of_content_should_generate_unique_anchors
1154 def test_table_of_content_should_generate_unique_anchors
1155 raw = <<-RAW
1155 raw = <<-RAW
1156 {{toc}}
1156 {{toc}}
1157
1157
1158 h1. Title
1158 h1. Title
1159
1159
1160 h2. Subtitle
1160 h2. Subtitle
1161
1161
1162 h2. Subtitle
1162 h2. Subtitle
1163 RAW
1163 RAW
1164
1164
1165 expected = '<ul class="toc">' +
1165 expected = '<ul class="toc">' +
1166 '<li><a href="#Title">Title</a>' +
1166 '<li><a href="#Title">Title</a>' +
1167 '<ul>' +
1167 '<ul>' +
1168 '<li><a href="#Subtitle">Subtitle</a></li>' +
1168 '<li><a href="#Subtitle">Subtitle</a></li>' +
1169 '<li><a href="#Subtitle-2">Subtitle</a></li>' +
1169 '<li><a href="#Subtitle-2">Subtitle</a></li>' +
1170 '</ul>' +
1170 '</ul>' +
1171 '</li>' +
1171 '</li>' +
1172 '</ul>'
1172 '</ul>'
1173
1173
1174 @project = Project.find(1)
1174 @project = Project.find(1)
1175 result = textilizable(raw).gsub("\n", "")
1175 result = textilizable(raw).gsub("\n", "")
1176 assert_include expected, result
1176 assert_include expected, result
1177 assert_include '<a name="Subtitle">', result
1177 assert_include '<a name="Subtitle">', result
1178 assert_include '<a name="Subtitle-2">', result
1178 assert_include '<a name="Subtitle-2">', result
1179 end
1179 end
1180
1180
1181 def test_table_of_content_should_contain_included_page_headings
1181 def test_table_of_content_should_contain_included_page_headings
1182 raw = <<-RAW
1182 raw = <<-RAW
1183 {{toc}}
1183 {{toc}}
1184
1184
1185 h1. Included
1185 h1. Included
1186
1186
1187 {{include(Child_1)}}
1187 {{include(Child_1)}}
1188 RAW
1188 RAW
1189
1189
1190 expected = '<ul class="toc">' +
1190 expected = '<ul class="toc">' +
1191 '<li><a href="#Included">Included</a></li>' +
1191 '<li><a href="#Included">Included</a></li>' +
1192 '<li><a href="#Child-page-1">Child page 1</a></li>' +
1192 '<li><a href="#Child-page-1">Child page 1</a></li>' +
1193 '</ul>'
1193 '</ul>'
1194
1194
1195 @project = Project.find(1)
1195 @project = Project.find(1)
1196 assert textilizable(raw).gsub("\n", "").include?(expected)
1196 assert textilizable(raw).gsub("\n", "").include?(expected)
1197 end
1197 end
1198
1198
1199 def test_toc_with_textile_formatting_should_be_parsed
1199 def test_toc_with_textile_formatting_should_be_parsed
1200 with_settings :text_formatting => 'textile' do
1200 with_settings :text_formatting => 'textile' do
1201 assert_select_in textilizable("{{toc}}\n\nh1. Heading"), 'ul.toc li', :text => 'Heading'
1201 assert_select_in textilizable("{{toc}}\n\nh1. Heading"), 'ul.toc li', :text => 'Heading'
1202 assert_select_in textilizable("{{<toc}}\n\nh1. Heading"), 'ul.toc.left li', :text => 'Heading'
1202 assert_select_in textilizable("{{<toc}}\n\nh1. Heading"), 'ul.toc.left li', :text => 'Heading'
1203 assert_select_in textilizable("{{>toc}}\n\nh1. Heading"), 'ul.toc.right li', :text => 'Heading'
1203 assert_select_in textilizable("{{>toc}}\n\nh1. Heading"), 'ul.toc.right li', :text => 'Heading'
1204 end
1204 end
1205 end
1205 end
1206
1206
1207 if Object.const_defined?(:Redcarpet)
1207 if Object.const_defined?(:Redcarpet)
1208 def test_toc_with_markdown_formatting_should_be_parsed
1208 def test_toc_with_markdown_formatting_should_be_parsed
1209 with_settings :text_formatting => 'markdown' do
1209 with_settings :text_formatting => 'markdown' do
1210 assert_select_in textilizable("{{toc}}\n\n# Heading"), 'ul.toc li', :text => 'Heading'
1210 assert_select_in textilizable("{{toc}}\n\n# Heading"), 'ul.toc li', :text => 'Heading'
1211 assert_select_in textilizable("{{<toc}}\n\n# Heading"), 'ul.toc.left li', :text => 'Heading'
1211 assert_select_in textilizable("{{<toc}}\n\n# Heading"), 'ul.toc.left li', :text => 'Heading'
1212 assert_select_in textilizable("{{>toc}}\n\n# Heading"), 'ul.toc.right li', :text => 'Heading'
1212 assert_select_in textilizable("{{>toc}}\n\n# Heading"), 'ul.toc.right li', :text => 'Heading'
1213 end
1213 end
1214 end
1214 end
1215 end
1215 end
1216
1216
1217 def test_section_edit_links
1217 def test_section_edit_links
1218 raw = <<-RAW
1218 raw = <<-RAW
1219 h1. Title
1219 h1. Title
1220
1220
1221 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1221 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
1222
1222
1223 h2. Subtitle with a [[Wiki]] link
1223 h2. Subtitle with a [[Wiki]] link
1224
1224
1225 h2. Subtitle with *some* _modifiers_
1225 h2. Subtitle with *some* _modifiers_
1226
1226
1227 h2. Subtitle with @inline code@
1227 h2. Subtitle with @inline code@
1228
1228
1229 <pre>
1229 <pre>
1230 some code
1230 some code
1231
1231
1232 h2. heading inside pre
1232 h2. heading inside pre
1233
1233
1234 <h2>html heading inside pre</h2>
1234 <h2>html heading inside pre</h2>
1235 </pre>
1235 </pre>
1236
1236
1237 h2. Subtitle after pre tag
1237 h2. Subtitle after pre tag
1238 RAW
1238 RAW
1239
1239
1240 @project = Project.find(1)
1240 @project = Project.find(1)
1241 set_language_if_valid 'en'
1241 set_language_if_valid 'en'
1242 result = textilizable(raw, :edit_section_links => {:controller => 'wiki', :action => 'edit', :project_id => '1', :id => 'Test'}).gsub("\n", "")
1242 result = textilizable(raw, :edit_section_links => {:controller => 'wiki', :action => 'edit', :project_id => '1', :id => 'Test'}).gsub("\n", "")
1243
1243
1244 # heading that contains inline code
1244 # heading that contains inline code
1245 assert_match Regexp.new('<div class="contextual heading-2" title="Edit this section" id="section-4">' +
1245 assert_match Regexp.new('<div class="contextual heading-2" title="Edit this section" id="section-4">' +
1246 '<a class="icon-only icon-edit" href="/projects/1/wiki/Test/edit\?section=4">Edit this section</a></div>' +
1246 '<a class="icon-only icon-edit" href="/projects/1/wiki/Test/edit\?section=4">Edit this section</a></div>' +
1247 '<a name="Subtitle-with-inline-code"></a>' +
1247 '<a name="Subtitle-with-inline-code"></a>' +
1248 '<h2 >Subtitle with <code>inline code</code><a href="#Subtitle-with-inline-code" class="wiki-anchor">&para;</a></h2>'),
1248 '<h2 >Subtitle with <code>inline code</code><a href="#Subtitle-with-inline-code" class="wiki-anchor">&para;</a></h2>'),
1249 result
1249 result
1250
1250
1251 # last heading
1251 # last heading
1252 assert_match Regexp.new('<div class="contextual heading-2" title="Edit this section" id="section-5">' +
1252 assert_match Regexp.new('<div class="contextual heading-2" title="Edit this section" id="section-5">' +
1253 '<a class="icon-only icon-edit" href="/projects/1/wiki/Test/edit\?section=5">Edit this section</a></div>' +
1253 '<a class="icon-only icon-edit" href="/projects/1/wiki/Test/edit\?section=5">Edit this section</a></div>' +
1254 '<a name="Subtitle-after-pre-tag"></a>' +
1254 '<a name="Subtitle-after-pre-tag"></a>' +
1255 '<h2 >Subtitle after pre tag<a href="#Subtitle-after-pre-tag" class="wiki-anchor">&para;</a></h2>'),
1255 '<h2 >Subtitle after pre tag<a href="#Subtitle-after-pre-tag" class="wiki-anchor">&para;</a></h2>'),
1256 result
1256 result
1257 end
1257 end
1258
1258
1259 def test_default_formatter
1259 def test_default_formatter
1260 with_settings :text_formatting => 'unknown' do
1260 with_settings :text_formatting => 'unknown' do
1261 text = 'a *link*: http://www.example.net/'
1261 text = 'a *link*: http://www.example.net/'
1262 assert_equal '<p>a *link*: <a class="external" href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
1262 assert_equal '<p>a *link*: <a class="external" href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
1263 end
1263 end
1264 end
1264 end
1265
1265
1266 def test_parse_redmine_links_should_handle_a_tag_without_attributes
1266 def test_parse_redmine_links_should_handle_a_tag_without_attributes
1267 text = '<a>http://example.com</a>'
1267 text = '<a>http://example.com</a>'
1268 expected = text.dup
1268 expected = text.dup
1269 parse_redmine_links(text, nil, nil, nil, true, {})
1269 parse_redmine_links(text, nil, nil, nil, true, {})
1270 assert_equal expected, text
1270 assert_equal expected, text
1271 end
1271 end
1272
1272
1273 def test_due_date_distance_in_words
1273 def test_due_date_distance_in_words
1274 to_test = { Date.today => 'Due in 0 days',
1274 to_test = { Date.today => 'Due in 0 days',
1275 Date.today + 1 => 'Due in 1 day',
1275 Date.today + 1 => 'Due in 1 day',
1276 Date.today + 100 => 'Due in about 3 months',
1276 Date.today + 100 => 'Due in about 3 months',
1277 Date.today + 20000 => 'Due in over 54 years',
1277 Date.today + 20000 => 'Due in over 54 years',
1278 Date.today - 1 => '1 day late',
1278 Date.today - 1 => '1 day late',
1279 Date.today - 100 => 'about 3 months late',
1279 Date.today - 100 => 'about 3 months late',
1280 Date.today - 20000 => 'over 54 years late',
1280 Date.today - 20000 => 'over 54 years late',
1281 }
1281 }
1282 ::I18n.locale = :en
1282 ::I18n.locale = :en
1283 to_test.each do |date, expected|
1283 to_test.each do |date, expected|
1284 assert_equal expected, due_date_distance_in_words(date)
1284 assert_equal expected, due_date_distance_in_words(date)
1285 end
1285 end
1286 end
1286 end
1287
1287
1288 def test_avatar_enabled
1288 def test_avatar_enabled
1289 with_settings :gravatar_enabled => '1' do
1289 with_settings :gravatar_enabled => '1' do
1290 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1290 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1291 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1291 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
1292 # Default size is 50
1292 # Default size is 50
1293 assert avatar('jsmith <jsmith@somenet.foo>').include?('size=50')
1293 assert avatar('jsmith <jsmith@somenet.foo>').include?('size=50')
1294 assert avatar('jsmith <jsmith@somenet.foo>', :size => 24).include?('size=24')
1294 assert avatar('jsmith <jsmith@somenet.foo>', :size => 24).include?('size=24')
1295 # Non-avatar options should be considered html options
1295 # Non-avatar options should be considered html options
1296 assert avatar('jsmith <jsmith@somenet.foo>', :title => 'John Smith').include?('title="John Smith"')
1296 assert avatar('jsmith <jsmith@somenet.foo>', :title => 'John Smith').include?('title="John Smith"')
1297 # The default class of the img tag should be gravatar
1297 # The default class of the img tag should be gravatar
1298 assert avatar('jsmith <jsmith@somenet.foo>').include?('class="gravatar"')
1298 assert avatar('jsmith <jsmith@somenet.foo>').include?('class="gravatar"')
1299 assert !avatar('jsmith <jsmith@somenet.foo>', :class => 'picture').include?('class="gravatar"')
1299 assert !avatar('jsmith <jsmith@somenet.foo>', :class => 'picture').include?('class="gravatar"')
1300 assert_nil avatar('jsmith')
1300 assert_nil avatar('jsmith')
1301 assert_nil avatar(nil)
1301 assert_nil avatar(nil)
1302 end
1302 end
1303 end
1303 end
1304
1304
1305 def test_avatar_disabled
1305 def test_avatar_disabled
1306 with_settings :gravatar_enabled => '0' do
1306 with_settings :gravatar_enabled => '0' do
1307 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
1307 assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
1308 end
1308 end
1309 end
1309 end
1310
1310
1311 def test_link_to_user
1311 def test_link_to_user
1312 user = User.find(2)
1312 user = User.find(2)
1313 result = link_to("John Smith", "/users/2", :class => "user active")
1313 result = link_to("John Smith", "/users/2", :class => "user active")
1314 assert_equal result, link_to_user(user)
1314 assert_equal result, link_to_user(user)
1315 end
1315 end
1316
1316
1317 def test_link_to_user_should_not_link_to_locked_user
1317 def test_link_to_user_should_not_link_to_locked_user
1318 with_current_user nil do
1318 with_current_user nil do
1319 user = User.find(5)
1319 user = User.find(5)
1320 assert user.locked?
1320 assert user.locked?
1321 assert_equal 'Dave2 Lopper2', link_to_user(user)
1321 assert_equal 'Dave2 Lopper2', link_to_user(user)
1322 end
1322 end
1323 end
1323 end
1324
1324
1325 def test_link_to_user_should_link_to_locked_user_if_current_user_is_admin
1325 def test_link_to_user_should_link_to_locked_user_if_current_user_is_admin
1326 with_current_user User.find(1) do
1326 with_current_user User.find(1) do
1327 user = User.find(5)
1327 user = User.find(5)
1328 assert user.locked?
1328 assert user.locked?
1329 result = link_to("Dave2 Lopper2", "/users/5", :class => "user locked")
1329 result = link_to("Dave2 Lopper2", "/users/5", :class => "user locked")
1330 assert_equal result, link_to_user(user)
1330 assert_equal result, link_to_user(user)
1331 end
1331 end
1332 end
1332 end
1333
1333
1334 def test_link_to_user_should_not_link_to_anonymous
1334 def test_link_to_user_should_not_link_to_anonymous
1335 user = User.anonymous
1335 user = User.anonymous
1336 assert user.anonymous?
1336 assert user.anonymous?
1337 t = link_to_user(user)
1337 t = link_to_user(user)
1338 assert_equal ::I18n.t(:label_user_anonymous), t
1338 assert_equal ::I18n.t(:label_user_anonymous), t
1339 end
1339 end
1340
1340
1341 def test_link_to_attachment
1341 def test_link_to_attachment
1342 a = Attachment.find(3)
1342 a = Attachment.find(3)
1343 assert_equal '<a href="/attachments/3/logo.gif">logo.gif</a>',
1343 assert_equal '<a href="/attachments/3/logo.gif">logo.gif</a>',
1344 link_to_attachment(a)
1344 link_to_attachment(a)
1345 assert_equal '<a href="/attachments/3/logo.gif">Text</a>',
1345 assert_equal '<a href="/attachments/3/logo.gif">Text</a>',
1346 link_to_attachment(a, :text => 'Text')
1346 link_to_attachment(a, :text => 'Text')
1347 result = link_to("logo.gif", "/attachments/3/logo.gif", :class => "foo")
1347 result = link_to("logo.gif", "/attachments/3/logo.gif", :class => "foo")
1348 assert_equal result,
1348 assert_equal result,
1349 link_to_attachment(a, :class => 'foo')
1349 link_to_attachment(a, :class => 'foo')
1350 assert_equal '<a href="/attachments/download/3/logo.gif">logo.gif</a>',
1350 assert_equal '<a href="/attachments/download/3/logo.gif">logo.gif</a>',
1351 link_to_attachment(a, :download => true)
1351 link_to_attachment(a, :download => true)
1352 assert_equal '<a href="http://test.host/attachments/3/logo.gif">logo.gif</a>',
1352 assert_equal '<a href="http://test.host/attachments/3/logo.gif">logo.gif</a>',
1353 link_to_attachment(a, :only_path => false)
1353 link_to_attachment(a, :only_path => false)
1354 end
1354 end
1355
1355
1356 def test_thumbnail_tag
1356 def test_thumbnail_tag
1357 a = Attachment.find(3)
1357 a = Attachment.find(3)
1358 assert_select_in thumbnail_tag(a),
1358 assert_select_in thumbnail_tag(a),
1359 'a[href=?][title=?] img[alt="3"][src=?]',
1359 'a[href=?][title=?] img[alt="3"][src=?]',
1360 "/attachments/3/logo.gif", "logo.gif", "/attachments/thumbnail/3"
1360 "/attachments/3/logo.gif", "logo.gif", "/attachments/thumbnail/3"
1361 end
1361 end
1362
1362
1363 def test_link_to_project
1363 def test_link_to_project
1364 project = Project.find(1)
1364 project = Project.find(1)
1365 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
1365 assert_equal %(<a href="/projects/ecookbook">eCookbook</a>),
1366 link_to_project(project)
1366 link_to_project(project)
1367 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
1367 assert_equal %(<a href="http://test.host/projects/ecookbook?jump=blah">eCookbook</a>),
1368 link_to_project(project, {:only_path => false, :jump => 'blah'})
1368 link_to_project(project, {:only_path => false, :jump => 'blah'})
1369 end
1369 end
1370
1370
1371 def test_link_to_project_settings
1371 def test_link_to_project_settings
1372 project = Project.find(1)
1372 project = Project.find(1)
1373 assert_equal '<a href="/projects/ecookbook/settings">eCookbook</a>', link_to_project_settings(project)
1373 assert_equal '<a href="/projects/ecookbook/settings">eCookbook</a>', link_to_project_settings(project)
1374
1374
1375 project.status = Project::STATUS_CLOSED
1375 project.status = Project::STATUS_CLOSED
1376 assert_equal '<a href="/projects/ecookbook">eCookbook</a>', link_to_project_settings(project)
1376 assert_equal '<a href="/projects/ecookbook">eCookbook</a>', link_to_project_settings(project)
1377
1377
1378 project.status = Project::STATUS_ARCHIVED
1378 project.status = Project::STATUS_ARCHIVED
1379 assert_equal 'eCookbook', link_to_project_settings(project)
1379 assert_equal 'eCookbook', link_to_project_settings(project)
1380 end
1380 end
1381
1381
1382 def test_link_to_legacy_project_with_numerical_identifier_should_use_id
1382 def test_link_to_legacy_project_with_numerical_identifier_should_use_id
1383 # numeric identifier are no longer allowed
1383 # numeric identifier are no longer allowed
1384 Project.where(:id => 1).update_all(:identifier => 25)
1384 Project.where(:id => 1).update_all(:identifier => 25)
1385 assert_equal '<a href="/projects/1">eCookbook</a>',
1385 assert_equal '<a href="/projects/1">eCookbook</a>',
1386 link_to_project(Project.find(1))
1386 link_to_project(Project.find(1))
1387 end
1387 end
1388
1388
1389 def test_principals_options_for_select_with_users
1389 def test_principals_options_for_select_with_users
1390 User.current = nil
1390 User.current = nil
1391 users = [User.find(2), User.find(4)]
1391 users = [User.find(2), User.find(4)]
1392 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
1392 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>),
1393 principals_options_for_select(users)
1393 principals_options_for_select(users)
1394 end
1394 end
1395
1395
1396 def test_principals_options_for_select_with_selected
1396 def test_principals_options_for_select_with_selected
1397 User.current = nil
1397 User.current = nil
1398 users = [User.find(2), User.find(4)]
1398 users = [User.find(2), User.find(4)]
1399 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
1399 assert_equal %(<option value="2">John Smith</option><option value="4" selected="selected">Robert Hill</option>),
1400 principals_options_for_select(users, User.find(4))
1400 principals_options_for_select(users, User.find(4))
1401 end
1401 end
1402
1402
1403 def test_principals_options_for_select_with_users_and_groups
1403 def test_principals_options_for_select_with_users_and_groups
1404 User.current = nil
1404 User.current = nil
1405 set_language_if_valid 'en'
1405 set_language_if_valid 'en'
1406 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
1406 users = [User.find(2), Group.find(11), User.find(4), Group.find(10)]
1407 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
1407 assert_equal %(<option value="2">John Smith</option><option value="4">Robert Hill</option>) +
1408 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
1408 %(<optgroup label="Groups"><option value="10">A Team</option><option value="11">B Team</option></optgroup>),
1409 principals_options_for_select(users)
1409 principals_options_for_select(users)
1410 end
1410 end
1411
1411
1412 def test_principals_options_for_select_with_empty_collection
1412 def test_principals_options_for_select_with_empty_collection
1413 assert_equal '', principals_options_for_select([])
1413 assert_equal '', principals_options_for_select([])
1414 end
1414 end
1415
1415
1416 def test_principals_options_for_select_should_include_me_option_when_current_user_is_in_collection
1416 def test_principals_options_for_select_should_include_me_option_when_current_user_is_in_collection
1417 set_language_if_valid 'en'
1417 set_language_if_valid 'en'
1418 users = [User.find(2), User.find(4)]
1418 users = [User.find(2), User.find(4)]
1419 User.current = User.find(4)
1419 User.current = User.find(4)
1420 assert_include '<option value="4">&lt;&lt; me &gt;&gt;</option>', principals_options_for_select(users)
1420 assert_include '<option value="4">&lt;&lt; me &gt;&gt;</option>', principals_options_for_select(users)
1421 end
1421 end
1422
1422
1423 def test_stylesheet_link_tag_should_pick_the_default_stylesheet
1423 def test_stylesheet_link_tag_should_pick_the_default_stylesheet
1424 assert_match 'href="/stylesheets/styles.css"', stylesheet_link_tag("styles")
1424 assert_match 'href="/stylesheets/styles.css"', stylesheet_link_tag("styles")
1425 end
1425 end
1426
1426
1427 def test_stylesheet_link_tag_for_plugin_should_pick_the_plugin_stylesheet
1427 def test_stylesheet_link_tag_for_plugin_should_pick_the_plugin_stylesheet
1428 assert_match 'href="/plugin_assets/foo/stylesheets/styles.css"', stylesheet_link_tag("styles", :plugin => :foo)
1428 assert_match 'href="/plugin_assets/foo/stylesheets/styles.css"', stylesheet_link_tag("styles", :plugin => :foo)
1429 end
1429 end
1430
1430
1431 def test_image_tag_should_pick_the_default_image
1431 def test_image_tag_should_pick_the_default_image
1432 assert_match 'src="/images/image.png"', image_tag("image.png")
1432 assert_match 'src="/images/image.png"', image_tag("image.png")
1433 end
1433 end
1434
1434
1435 def test_image_tag_should_pick_the_theme_image_if_it_exists
1435 def test_image_tag_should_pick_the_theme_image_if_it_exists
1436 theme = Redmine::Themes.themes.last
1436 theme = Redmine::Themes.themes.last
1437 theme.images << 'image.png'
1437 theme.images << 'image.png'
1438
1438
1439 with_settings :ui_theme => theme.id do
1439 with_settings :ui_theme => theme.id do
1440 assert_match %|src="/themes/#{theme.dir}/images/image.png"|, image_tag("image.png")
1440 assert_match %|src="/themes/#{theme.dir}/images/image.png"|, image_tag("image.png")
1441 assert_match %|src="/images/other.png"|, image_tag("other.png")
1441 assert_match %|src="/images/other.png"|, image_tag("other.png")
1442 end
1442 end
1443 ensure
1443 ensure
1444 theme.images.delete 'image.png'
1444 theme.images.delete 'image.png'
1445 end
1445 end
1446
1446
1447 def test_image_tag_sfor_plugin_should_pick_the_plugin_image
1447 def test_image_tag_sfor_plugin_should_pick_the_plugin_image
1448 assert_match 'src="/plugin_assets/foo/images/image.png"', image_tag("image.png", :plugin => :foo)
1448 assert_match 'src="/plugin_assets/foo/images/image.png"', image_tag("image.png", :plugin => :foo)
1449 end
1449 end
1450
1450
1451 def test_javascript_include_tag_should_pick_the_default_javascript
1451 def test_javascript_include_tag_should_pick_the_default_javascript
1452 assert_match 'src="/javascripts/scripts.js"', javascript_include_tag("scripts")
1452 assert_match 'src="/javascripts/scripts.js"', javascript_include_tag("scripts")
1453 end
1453 end
1454
1454
1455 def test_javascript_include_tag_for_plugin_should_pick_the_plugin_javascript
1455 def test_javascript_include_tag_for_plugin_should_pick_the_plugin_javascript
1456 assert_match 'src="/plugin_assets/foo/javascripts/scripts.js"', javascript_include_tag("scripts", :plugin => :foo)
1456 assert_match 'src="/plugin_assets/foo/javascripts/scripts.js"', javascript_include_tag("scripts", :plugin => :foo)
1457 end
1457 end
1458
1458
1459 def test_raw_json_should_escape_closing_tags
1459 def test_raw_json_should_escape_closing_tags
1460 s = raw_json(["<foo>bar</foo>"])
1460 s = raw_json(["<foo>bar</foo>"])
1461 assert_include '\/foo', s
1461 assert_include '\/foo', s
1462 end
1462 end
1463
1463
1464 def test_raw_json_should_be_html_safe
1464 def test_raw_json_should_be_html_safe
1465 s = raw_json(["foo"])
1465 s = raw_json(["foo"])
1466 assert s.html_safe?
1466 assert s.html_safe?
1467 end
1467 end
1468
1468
1469 def test_html_title_should_app_title_if_not_set
1469 def test_html_title_should_app_title_if_not_set
1470 assert_equal 'Redmine', html_title
1470 assert_equal 'Redmine', html_title
1471 end
1471 end
1472
1472
1473 def test_html_title_should_join_items
1473 def test_html_title_should_join_items
1474 html_title 'Foo', 'Bar'
1474 html_title 'Foo', 'Bar'
1475 assert_equal 'Foo - Bar - Redmine', html_title
1475 assert_equal 'Foo - Bar - Redmine', html_title
1476 end
1476 end
1477
1477
1478 def test_html_title_should_append_current_project_name
1478 def test_html_title_should_append_current_project_name
1479 @project = Project.find(1)
1479 @project = Project.find(1)
1480 html_title 'Foo', 'Bar'
1480 html_title 'Foo', 'Bar'
1481 assert_equal 'Foo - Bar - eCookbook - Redmine', html_title
1481 assert_equal 'Foo - Bar - eCookbook - Redmine', html_title
1482 end
1482 end
1483
1483
1484 def test_title_should_return_a_h2_tag
1484 def test_title_should_return_a_h2_tag
1485 assert_equal '<h2>Foo</h2>', title('Foo')
1485 assert_equal '<h2>Foo</h2>', title('Foo')
1486 end
1486 end
1487
1487
1488 def test_title_should_set_html_title
1488 def test_title_should_set_html_title
1489 title('Foo')
1489 title('Foo')
1490 assert_equal 'Foo - Redmine', html_title
1490 assert_equal 'Foo - Redmine', html_title
1491 end
1491 end
1492
1492
1493 def test_title_should_turn_arrays_into_links
1493 def test_title_should_turn_arrays_into_links
1494 assert_equal '<h2><a href="/foo">Foo</a></h2>', title(['Foo', '/foo'])
1494 assert_equal '<h2><a href="/foo">Foo</a></h2>', title(['Foo', '/foo'])
1495 assert_equal 'Foo - Redmine', html_title
1495 assert_equal 'Foo - Redmine', html_title
1496 end
1496 end
1497
1497
1498 def test_title_should_join_items
1498 def test_title_should_join_items
1499 assert_equal '<h2>Foo &#187; Bar</h2>', title('Foo', 'Bar')
1499 assert_equal '<h2>Foo &#187; Bar</h2>', title('Foo', 'Bar')
1500 assert_equal 'Bar - Foo - Redmine', html_title
1500 assert_equal 'Bar - Foo - Redmine', html_title
1501 end
1501 end
1502
1502
1503 def test_favicon_path
1503 def test_favicon_path
1504 assert_match %r{^/favicon\.ico}, favicon_path
1504 assert_match %r{^/favicon\.ico}, favicon_path
1505 end
1505 end
1506
1506
1507 def test_favicon_path_with_suburi
1507 def test_favicon_path_with_suburi
1508 Redmine::Utils.relative_url_root = '/foo'
1508 Redmine::Utils.relative_url_root = '/foo'
1509 assert_match %r{^/foo/favicon\.ico}, favicon_path
1509 assert_match %r{^/foo/favicon\.ico}, favicon_path
1510 ensure
1510 ensure
1511 Redmine::Utils.relative_url_root = ''
1511 Redmine::Utils.relative_url_root = ''
1512 end
1512 end
1513
1513
1514 def test_favicon_url
1514 def test_favicon_url
1515 assert_match %r{^http://test\.host/favicon\.ico}, favicon_url
1515 assert_match %r{^http://test\.host/favicon\.ico}, favicon_url
1516 end
1516 end
1517
1517
1518 def test_favicon_url_with_suburi
1518 def test_favicon_url_with_suburi
1519 Redmine::Utils.relative_url_root = '/foo'
1519 Redmine::Utils.relative_url_root = '/foo'
1520 assert_match %r{^http://test\.host/foo/favicon\.ico}, favicon_url
1520 assert_match %r{^http://test\.host/foo/favicon\.ico}, favicon_url
1521 ensure
1521 ensure
1522 Redmine::Utils.relative_url_root = ''
1522 Redmine::Utils.relative_url_root = ''
1523 end
1523 end
1524
1524
1525 def test_truncate_single_line
1525 def test_truncate_single_line
1526 str = "01234"
1526 str = "01234"
1527 result = truncate_single_line_raw("#{str}\n#{str}", 10)
1527 result = truncate_single_line_raw("#{str}\n#{str}", 10)
1528 assert_equal "01234 0...", result
1528 assert_equal "01234 0...", result
1529 assert !result.html_safe?
1529 assert !result.html_safe?
1530 result = truncate_single_line_raw("#{str}<&#>\n#{str}#{str}", 16)
1530 result = truncate_single_line_raw("#{str}<&#>\n#{str}#{str}", 16)
1531 assert_equal "01234<&#> 012...", result
1531 assert_equal "01234<&#> 012...", result
1532 assert !result.html_safe?
1532 assert !result.html_safe?
1533 end
1533 end
1534
1534
1535 def test_truncate_single_line_non_ascii
1535 def test_truncate_single_line_non_ascii
1536 ja = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e".force_encoding('UTF-8')
1536 ja = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e".force_encoding('UTF-8')
1537 result = truncate_single_line_raw("#{ja}\n#{ja}\n#{ja}", 10)
1537 result = truncate_single_line_raw("#{ja}\n#{ja}\n#{ja}", 10)
1538 assert_equal "#{ja} #{ja}...", result
1538 assert_equal "#{ja} #{ja}...", result
1539 assert !result.html_safe?
1539 assert !result.html_safe?
1540 end
1540 end
1541 end
1541 end
General Comments 0
You need to be logged in to leave comments. Login now