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