##// END OF EJS Templates
Escape double-quotes in image titles....
Jean-Philippe Lang -
r2142:4ec5b1600a9e
parent child
Show More
@@ -1,1168 +1,1172
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 # <acronym title="American Civil Liberties Union">ACLU</acronym>
133 133 #
134 134 # == Adding Tables
135 135 #
136 136 # In Textile, simple tables can be added by seperating 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
169 169 VERSION = '3.0.4'
170 170 DEFAULT_RULES = [:textile, :markdown]
171 171
172 172 #
173 173 # Two accessor for setting security restrictions.
174 174 #
175 175 # This is a nice thing if you're using RedCloth for
176 176 # formatting in public places (e.g. Wikis) where you
177 177 # don't want users to abuse HTML for bad things.
178 178 #
179 179 # If +:filter_html+ is set, HTML which wasn't
180 180 # created by the Textile processor will be escaped.
181 181 #
182 182 # If +:filter_styles+ is set, it will also disable
183 183 # the style markup specifier. ('{color: red}')
184 184 #
185 185 attr_accessor :filter_html, :filter_styles
186 186
187 187 #
188 188 # Accessor for toggling hard breaks.
189 189 #
190 190 # If +:hard_breaks+ is set, single newlines will
191 191 # be converted to HTML break tags. This is the
192 192 # default behavior for traditional RedCloth.
193 193 #
194 194 attr_accessor :hard_breaks
195 195
196 196 # Accessor for toggling lite mode.
197 197 #
198 198 # In lite mode, block-level rules are ignored. This means
199 199 # that tables, paragraphs, lists, and such aren't available.
200 200 # Only the inline markup for bold, italics, entities and so on.
201 201 #
202 202 # r = RedCloth.new( "And then? She *fell*!", [:lite_mode] )
203 203 # r.to_html
204 204 # #=> "And then? She <strong>fell</strong>!"
205 205 #
206 206 attr_accessor :lite_mode
207 207
208 208 #
209 209 # Accessor for toggling span caps.
210 210 #
211 211 # Textile places `span' tags around capitalized
212 212 # words by default, but this wreaks havoc on Wikis.
213 213 # If +:no_span_caps+ is set, this will be
214 214 # suppressed.
215 215 #
216 216 attr_accessor :no_span_caps
217 217
218 218 #
219 219 # Establishes the markup predence. Available rules include:
220 220 #
221 221 # == Textile Rules
222 222 #
223 223 # The following textile rules can be set individually. Or add the complete
224 224 # set of rules with the single :textile rule, which supplies the rule set in
225 225 # the following precedence:
226 226 #
227 227 # refs_textile:: Textile references (i.e. [hobix]http://hobix.com/)
228 228 # block_textile_table:: Textile table block structures
229 229 # block_textile_lists:: Textile list structures
230 230 # block_textile_prefix:: Textile blocks with prefixes (i.e. bq., h2., etc.)
231 231 # inline_textile_image:: Textile inline images
232 232 # inline_textile_link:: Textile inline links
233 233 # inline_textile_span:: Textile inline spans
234 234 # glyphs_textile:: Textile entities (such as em-dashes and smart quotes)
235 235 #
236 236 # == Markdown
237 237 #
238 238 # refs_markdown:: Markdown references (for example: [hobix]: http://hobix.com/)
239 239 # block_markdown_setext:: Markdown setext headers
240 240 # block_markdown_atx:: Markdown atx headers
241 241 # block_markdown_rule:: Markdown horizontal rules
242 242 # block_markdown_bq:: Markdown blockquotes
243 243 # block_markdown_lists:: Markdown lists
244 244 # inline_markdown_link:: Markdown links
245 245 attr_accessor :rules
246 246
247 247 # Returns a new RedCloth object, based on _string_ and
248 248 # enforcing all the included _restrictions_.
249 249 #
250 250 # r = RedCloth.new( "h1. A <b>bold</b> man", [:filter_html] )
251 251 # r.to_html
252 252 # #=>"<h1>A &lt;b&gt;bold&lt;/b&gt; man</h1>"
253 253 #
254 254 def initialize( string, restrictions = [] )
255 255 restrictions.each { |r| method( "#{ r }=" ).call( true ) }
256 256 super( string )
257 257 end
258 258
259 259 #
260 260 # Generates HTML from the Textile contents.
261 261 #
262 262 # r = RedCloth.new( "And then? She *fell*!" )
263 263 # r.to_html( true )
264 264 # #=>"And then? She <strong>fell</strong>!"
265 265 #
266 266 def to_html( *rules )
267 267 rules = DEFAULT_RULES if rules.empty?
268 268 # make our working copy
269 269 text = self.dup
270 270
271 271 @urlrefs = {}
272 272 @shelf = []
273 273 textile_rules = [:refs_textile, :block_textile_table, :block_textile_lists,
274 274 :block_textile_prefix, :inline_textile_image, :inline_textile_link,
275 275 :inline_textile_code, :inline_textile_span, :glyphs_textile]
276 276 markdown_rules = [:refs_markdown, :block_markdown_setext, :block_markdown_atx, :block_markdown_rule,
277 277 :block_markdown_bq, :block_markdown_lists,
278 278 :inline_markdown_reflink, :inline_markdown_link]
279 279 @rules = rules.collect do |rule|
280 280 case rule
281 281 when :markdown
282 282 markdown_rules
283 283 when :textile
284 284 textile_rules
285 285 else
286 286 rule
287 287 end
288 288 end.flatten
289 289
290 290 # standard clean up
291 291 incoming_entities text
292 292 clean_white_space text
293 293
294 294 # start processor
295 295 @pre_list = []
296 296 rip_offtags text
297 297 no_textile text
298 298 escape_html_tags text
299 299 hard_break text
300 300 unless @lite_mode
301 301 refs text
302 302 # need to do this before text is split by #blocks
303 303 block_textile_quotes text
304 304 blocks text
305 305 end
306 306 inline text
307 307 smooth_offtags text
308 308
309 309 retrieve text
310 310
311 311 text.gsub!( /<\/?notextile>/, '' )
312 312 text.gsub!( /x%x%/, '&#38;' )
313 313 clean_html text if filter_html
314 314 text.strip!
315 315 text
316 316
317 317 end
318 318
319 319 #######
320 320 private
321 321 #######
322 322 #
323 323 # Mapping of 8-bit ASCII codes to HTML numerical entity equivalents.
324 324 # (from PyTextile)
325 325 #
326 326 TEXTILE_TAGS =
327 327
328 328 [[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230],
329 329 [134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249],
330 330 [140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217],
331 331 [147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732],
332 332 [153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]].
333 333
334 334 collect! do |a, b|
335 335 [a.chr, ( b.zero? and "" or "&#{ b };" )]
336 336 end
337 337
338 338 #
339 339 # Regular expressions to convert to HTML.
340 340 #
341 341 A_HLGN = /(?:(?:<>|<|>|\=|[()]+)+)/
342 342 A_VLGN = /[\-^~]/
343 343 C_CLAS = '(?:\([^)]+\))'
344 344 C_LNGE = '(?:\[[^\[\]]+\])'
345 345 C_STYL = '(?:\{[^}]+\})'
346 346 S_CSPN = '(?:\\\\\d+)'
347 347 S_RSPN = '(?:/\d+)'
348 348 A = "(?:#{A_HLGN}?#{A_VLGN}?|#{A_VLGN}?#{A_HLGN}?)"
349 349 S = "(?:#{S_CSPN}?#{S_RSPN}|#{S_RSPN}?#{S_CSPN}?)"
350 350 C = "(?:#{C_CLAS}?#{C_STYL}?#{C_LNGE}?|#{C_STYL}?#{C_LNGE}?#{C_CLAS}?|#{C_LNGE}?#{C_STYL}?#{C_CLAS}?)"
351 351 # PUNCT = Regexp::quote( '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' )
352 352 PUNCT = Regexp::quote( '!"#$%&\'*+,-./:;=?@\\^_`|~' )
353 353 PUNCT_NOQ = Regexp::quote( '!"#$&\',./:;=?@\\`|' )
354 354 PUNCT_Q = Regexp::quote( '*-_+^~%' )
355 355 HYPERLINK = '(\S+?)([^\w\s/;=\?]*?)(?=\s|<|$)'
356 356
357 357 # Text markup tags, don't conflict with block tags
358 358 SIMPLE_HTML_TAGS = [
359 359 'tt', 'b', 'i', 'big', 'small', 'em', 'strong', 'dfn', 'code',
360 360 'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'a', 'img', 'br',
361 361 'br', 'map', 'q', 'sub', 'sup', 'span', 'bdo'
362 362 ]
363 363
364 364 QTAGS = [
365 365 ['**', 'b', :limit],
366 366 ['*', 'strong', :limit],
367 367 ['??', 'cite', :limit],
368 368 ['-', 'del', :limit],
369 369 ['__', 'i', :limit],
370 370 ['_', 'em', :limit],
371 371 ['%', 'span', :limit],
372 372 ['+', 'ins', :limit],
373 373 ['^', 'sup', :limit],
374 374 ['~', 'sub', :limit]
375 375 ]
376 376 QTAGS.collect! do |rc, ht, rtype|
377 377 rcq = Regexp::quote rc
378 378 re =
379 379 case rtype
380 380 when :limit
381 381 /(^|[>\s\(])
382 382 (#{rcq})
383 383 (#{C})
384 384 (?::(\S+?))?
385 385 ([^\s\-].*?[^\s\-]|\w)
386 386 #{rcq}
387 387 (?=[[:punct:]]|\s|\)|$)/x
388 388 else
389 389 /(#{rcq})
390 390 (#{C})
391 391 (?::(\S+))?
392 392 ([^\s\-].*?[^\s\-]|\w)
393 393 #{rcq}/xm
394 394 end
395 395 [rc, ht, re, rtype]
396 396 end
397 397
398 398 # Elements to handle
399 399 GLYPHS = [
400 400 # [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1&#8217;\2' ], # single closing
401 401 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)\'/, '\1&#8217;' ], # single closing
402 402 # [ /\'(?=[#{PUNCT_Q}]*(s\b|[\s#{PUNCT_NOQ}]))/, '&#8217;' ], # single closing
403 403 # [ /\'/, '&#8216;' ], # single opening
404 404 # [ /</, '&lt;' ], # less-than
405 405 # [ />/, '&gt;' ], # greater-than
406 406 # [ /([^\s\[{(])?"(\s|:|$)/, '\1&#8221;\2' ], # double closing
407 407 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)"/, '\1&#8221;' ], # double closing
408 408 # [ /"(?=[#{PUNCT_Q}]*[\s#{PUNCT_NOQ}])/, '&#8221;' ], # double closing
409 409 # [ /"/, '&#8220;' ], # double opening
410 410 # [ /\b( )?\.{3}/, '\1&#8230;' ], # ellipsis
411 411 [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '<acronym title="\2">\1</acronym>' ], # 3+ uppercase acronym
412 412 # [ /(^|[^"][>\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
413 413 # [ /(\.\s)?\s?--\s?/, '\1&#8212;' ], # em dash
414 414 # [ /\s->\s/, ' &rarr; ' ], # right arrow
415 415 # [ /\s-\s/, ' &#8211; ' ], # en dash
416 416 # [ /(\d+) ?x ?(\d+)/, '\1&#215;\2' ], # dimension sign
417 417 # [ /\b ?[(\[]TM[\])]/i, '&#8482;' ], # trademark
418 418 # [ /\b ?[(\[]R[\])]/i, '&#174;' ], # registered
419 419 # [ /\b ?[(\[]C[\])]/i, '&#169;' ] # copyright
420 420 ]
421 421
422 422 H_ALGN_VALS = {
423 423 '<' => 'left',
424 424 '=' => 'center',
425 425 '>' => 'right',
426 426 '<>' => 'justify'
427 427 }
428 428
429 429 V_ALGN_VALS = {
430 430 '^' => 'top',
431 431 '-' => 'middle',
432 432 '~' => 'bottom'
433 433 }
434 434
435 435 #
436 436 # Flexible HTML escaping
437 437 #
438 def htmlesc( str, mode )
438 def htmlesc( str, mode=:Quotes )
439 if str
439 440 str.gsub!( '&', '&amp;' )
440 441 str.gsub!( '"', '&quot;' ) if mode != :NoQuotes
441 442 str.gsub!( "'", '&#039;' ) if mode == :Quotes
442 443 str.gsub!( '<', '&lt;')
443 444 str.gsub!( '>', '&gt;')
445 end
446 str
444 447 end
445 448
446 449 # Search and replace for Textile glyphs (quotes, dashes, other symbols)
447 450 def pgl( text )
448 451 GLYPHS.each do |re, resub, tog|
449 452 next if tog and method( tog ).call
450 453 text.gsub! re, resub
451 454 end
452 455 end
453 456
454 457 # Parses Textile attribute lists and builds an HTML attribute string
455 458 def pba( text_in, element = "" )
456 459
457 460 return '' unless text_in
458 461
459 462 style = []
460 463 text = text_in.dup
461 464 if element == 'td'
462 465 colspan = $1 if text =~ /\\(\d+)/
463 466 rowspan = $1 if text =~ /\/(\d+)/
464 467 style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN
465 468 end
466 469
467 470 style << "#{ $1 };" if not filter_styles and
468 471 text.sub!( /\{([^}]*)\}/, '' )
469 472
470 473 lang = $1 if
471 474 text.sub!( /\[([^)]+?)\]/, '' )
472 475
473 476 cls = $1 if
474 477 text.sub!( /\(([^()]+?)\)/, '' )
475 478
476 479 style << "padding-left:#{ $1.length }em;" if
477 480 text.sub!( /([(]+)/, '' )
478 481
479 482 style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' )
480 483
481 484 style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN
482 485
483 486 cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/
484 487
485 488 atts = ''
486 489 atts << " style=\"#{ style.join }\"" unless style.empty?
487 490 atts << " class=\"#{ cls }\"" unless cls.to_s.empty?
488 491 atts << " lang=\"#{ lang }\"" if lang
489 492 atts << " id=\"#{ id }\"" if id
490 493 atts << " colspan=\"#{ colspan }\"" if colspan
491 494 atts << " rowspan=\"#{ rowspan }\"" if rowspan
492 495
493 496 atts
494 497 end
495 498
496 499 TABLE_RE = /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)(\n\n|\Z)/m
497 500
498 501 # Parses a Textile table block, building HTML from the result.
499 502 def block_textile_table( text )
500 503 text.gsub!( TABLE_RE ) do |matches|
501 504
502 505 tatts, fullrow = $~[1..2]
503 506 tatts = pba( tatts, 'table' )
504 507 tatts = shelve( tatts ) if tatts
505 508 rows = []
506 509
507 510 fullrow.each_line do |row|
508 511 ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m
509 512 cells = []
510 513 row.split( /(\|)(?![^\[\|]*\]\])/ )[1..-2].each do |cell|
511 514 next if cell == '|'
512 515 ctyp = 'd'
513 516 ctyp = 'h' if cell =~ /^_/
514 517
515 518 catts = ''
516 519 catts, cell = pba( $1, 'td' ), $2 if cell =~ /^(_?#{S}#{A}#{C}\. ?)(.*)/
517 520
518 521 catts = shelve( catts ) if catts
519 522 cells << "\t\t\t<t#{ ctyp }#{ catts }>#{ cell }</t#{ ctyp }>"
520 523 end
521 524 ratts = shelve( ratts ) if ratts
522 525 rows << "\t\t<tr#{ ratts }>\n#{ cells.join( "\n" ) }\n\t\t</tr>"
523 526 end
524 527 "\t<table#{ tatts }>\n#{ rows.join( "\n" ) }\n\t</table>\n\n"
525 528 end
526 529 end
527 530
528 531 LISTS_RE = /^([#*]+?#{C} .*?)$(?![^#*])/m
529 532 LISTS_CONTENT_RE = /^([#*]+)(#{A}#{C}) (.*)$/m
530 533
531 534 # Parses Textile lists and generates HTML
532 535 def block_textile_lists( text )
533 536 text.gsub!( LISTS_RE ) do |match|
534 537 lines = match.split( /\n/ )
535 538 last_line = -1
536 539 depth = []
537 540 lines.each_with_index do |line, line_id|
538 541 if line =~ LISTS_CONTENT_RE
539 542 tl,atts,content = $~[1..3]
540 543 if depth.last
541 544 if depth.last.length > tl.length
542 545 (depth.length - 1).downto(0) do |i|
543 546 break if depth[i].length == tl.length
544 547 lines[line_id - 1] << "</li>\n\t</#{ lT( depth[i] ) }l>\n\t"
545 548 depth.pop
546 549 end
547 550 end
548 551 if depth.last and depth.last.length == tl.length
549 552 lines[line_id - 1] << '</li>'
550 553 end
551 554 end
552 555 unless depth.last == tl
553 556 depth << tl
554 557 atts = pba( atts )
555 558 atts = shelve( atts ) if atts
556 559 lines[line_id] = "\t<#{ lT(tl) }l#{ atts }>\n\t<li>#{ content }"
557 560 else
558 561 lines[line_id] = "\t\t<li>#{ content }"
559 562 end
560 563 last_line = line_id
561 564
562 565 else
563 566 last_line = line_id
564 567 end
565 568 if line_id - last_line > 1 or line_id == lines.length - 1
566 569 depth.delete_if do |v|
567 570 lines[last_line] << "</li>\n\t</#{ lT( v ) }l>"
568 571 end
569 572 end
570 573 end
571 574 lines.join( "\n" )
572 575 end
573 576 end
574 577
575 578 QUOTES_RE = /(^>+([^\n]*?)\n?)+/m
576 579 QUOTES_CONTENT_RE = /^([> ]+)(.*)$/m
577 580
578 581 def block_textile_quotes( text )
579 582 text.gsub!( QUOTES_RE ) do |match|
580 583 lines = match.split( /\n/ )
581 584 quotes = ''
582 585 indent = 0
583 586 lines.each do |line|
584 587 line =~ QUOTES_CONTENT_RE
585 588 bq,content = $1, $2
586 589 l = bq.count('>')
587 590 if l != indent
588 591 quotes << ("\n\n" + (l>indent ? '<blockquote>' * (l-indent) : '</blockquote>' * (indent-l)) + "\n\n")
589 592 indent = l
590 593 end
591 594 quotes << (content + "\n")
592 595 end
593 596 quotes << ("\n" + '</blockquote>' * indent + "\n\n")
594 597 quotes
595 598 end
596 599 end
597 600
598 601 CODE_RE = /(\W)
599 602 @
600 603 (?:\|(\w+?)\|)?
601 604 (.+?)
602 605 @
603 606 (?=\W)/x
604 607
605 608 def inline_textile_code( text )
606 609 text.gsub!( CODE_RE ) do |m|
607 610 before,lang,code,after = $~[1..4]
608 611 lang = " lang=\"#{ lang }\"" if lang
609 612 rip_offtags( "#{ before }<code#{ lang }>#{ code }</code>#{ after }" )
610 613 end
611 614 end
612 615
613 616 def lT( text )
614 617 text =~ /\#$/ ? 'o' : 'u'
615 618 end
616 619
617 620 def hard_break( text )
618 621 text.gsub!( /(.)\n(?!\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
619 622 end
620 623
621 624 BLOCKS_GROUP_RE = /\n{2,}(?! )/m
622 625
623 626 def blocks( text, deep_code = false )
624 627 text.replace( text.split( BLOCKS_GROUP_RE ).collect do |blk|
625 628 plain = blk !~ /\A[#*> ]/
626 629
627 630 # skip blocks that are complex HTML
628 631 if blk =~ /^<\/?(\w+).*>/ and not SIMPLE_HTML_TAGS.include? $1
629 632 blk
630 633 else
631 634 # search for indentation levels
632 635 blk.strip!
633 636 if blk.empty?
634 637 blk
635 638 else
636 639 code_blk = nil
637 640 blk.gsub!( /((?:\n(?:\n^ +[^\n]*)+)+)/m ) do |iblk|
638 641 flush_left iblk
639 642 blocks iblk, plain
640 643 iblk.gsub( /^(\S)/, "\t\\1" )
641 644 if plain
642 645 code_blk = iblk; ""
643 646 else
644 647 iblk
645 648 end
646 649 end
647 650
648 651 block_applied = 0
649 652 @rules.each do |rule_name|
650 653 block_applied += 1 if ( rule_name.to_s.match /^block_/ and method( rule_name ).call( blk ) )
651 654 end
652 655 if block_applied.zero?
653 656 if deep_code
654 657 blk = "\t<pre><code>#{ blk }</code></pre>"
655 658 else
656 659 blk = "\t<p>#{ blk }</p>"
657 660 end
658 661 end
659 662 # hard_break blk
660 663 blk + "\n#{ code_blk }"
661 664 end
662 665 end
663 666
664 667 end.join( "\n\n" ) )
665 668 end
666 669
667 670 def textile_bq( tag, atts, cite, content )
668 671 cite, cite_title = check_refs( cite )
669 672 cite = " cite=\"#{ cite }\"" if cite
670 673 atts = shelve( atts ) if atts
671 674 "\t<blockquote#{ cite }>\n\t\t<p#{ atts }>#{ content }</p>\n\t</blockquote>"
672 675 end
673 676
674 677 def textile_p( tag, atts, cite, content )
675 678 atts = shelve( atts ) if atts
676 679 "\t<#{ tag }#{ atts }>#{ content }</#{ tag }>"
677 680 end
678 681
679 682 alias textile_h1 textile_p
680 683 alias textile_h2 textile_p
681 684 alias textile_h3 textile_p
682 685 alias textile_h4 textile_p
683 686 alias textile_h5 textile_p
684 687 alias textile_h6 textile_p
685 688
686 689 def textile_fn_( tag, num, atts, cite, content )
687 690 atts << " id=\"fn#{ num }\" class=\"footnote\""
688 691 content = "<sup>#{ num }</sup> #{ content }"
689 692 atts = shelve( atts ) if atts
690 693 "\t<p#{ atts }>#{ content }</p>"
691 694 end
692 695
693 696 BLOCK_RE = /^(([a-z]+)(\d*))(#{A}#{C})\.(?::(\S+))? (.*)$/m
694 697
695 698 def block_textile_prefix( text )
696 699 if text =~ BLOCK_RE
697 700 tag,tagpre,num,atts,cite,content = $~[1..6]
698 701 atts = pba( atts )
699 702
700 703 # pass to prefix handler
701 704 if respond_to? "textile_#{ tag }", true
702 705 text.gsub!( $&, method( "textile_#{ tag }" ).call( tag, atts, cite, content ) )
703 706 elsif respond_to? "textile_#{ tagpre }_", true
704 707 text.gsub!( $&, method( "textile_#{ tagpre }_" ).call( tagpre, num, atts, cite, content ) )
705 708 end
706 709 end
707 710 end
708 711
709 712 SETEXT_RE = /\A(.+?)\n([=-])[=-]* *$/m
710 713 def block_markdown_setext( text )
711 714 if text =~ SETEXT_RE
712 715 tag = if $2 == "="; "h1"; else; "h2"; end
713 716 blk, cont = "<#{ tag }>#{ $1 }</#{ tag }>", $'
714 717 blocks cont
715 718 text.replace( blk + cont )
716 719 end
717 720 end
718 721
719 722 ATX_RE = /\A(\#{1,6}) # $1 = string of #'s
720 723 [ ]*
721 724 (.+?) # $2 = Header text
722 725 [ ]*
723 726 \#* # optional closing #'s (not counted)
724 727 $/x
725 728 def block_markdown_atx( text )
726 729 if text =~ ATX_RE
727 730 tag = "h#{ $1.length }"
728 731 blk, cont = "<#{ tag }>#{ $2 }</#{ tag }>\n\n", $'
729 732 blocks cont
730 733 text.replace( blk + cont )
731 734 end
732 735 end
733 736
734 737 MARKDOWN_BQ_RE = /\A(^ *> ?.+$(.+\n)*\n*)+/m
735 738
736 739 def block_markdown_bq( text )
737 740 text.gsub!( MARKDOWN_BQ_RE ) do |blk|
738 741 blk.gsub!( /^ *> ?/, '' )
739 742 flush_left blk
740 743 blocks blk
741 744 blk.gsub!( /^(\S)/, "\t\\1" )
742 745 "<blockquote>\n#{ blk }\n</blockquote>\n\n"
743 746 end
744 747 end
745 748
746 749 MARKDOWN_RULE_RE = /^(#{
747 750 ['*', '-', '_'].collect { |ch| ' ?(' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' )
748 751 })$/
749 752
750 753 def block_markdown_rule( text )
751 754 text.gsub!( MARKDOWN_RULE_RE ) do |blk|
752 755 "<hr />"
753 756 end
754 757 end
755 758
756 759 # XXX TODO XXX
757 760 def block_markdown_lists( text )
758 761 end
759 762
760 763 def inline_textile_span( text )
761 764 QTAGS.each do |qtag_rc, ht, qtag_re, rtype|
762 765 text.gsub!( qtag_re ) do |m|
763 766
764 767 case rtype
765 768 when :limit
766 769 sta,qtag,atts,cite,content = $~[1..5]
767 770 else
768 771 qtag,atts,cite,content = $~[1..4]
769 772 sta = ''
770 773 end
771 774 atts = pba( atts )
772 775 atts << " cite=\"#{ cite }\"" if cite
773 776 atts = shelve( atts ) if atts
774 777
775 778 "#{ sta }<#{ ht }#{ atts }>#{ content }</#{ ht }>"
776 779
777 780 end
778 781 end
779 782 end
780 783
781 784 LINK_RE = /
782 785 ([\s\[{(]|[#{PUNCT}])? # $pre
783 786 " # start
784 787 (#{C}) # $atts
785 788 ([^"\n]+?) # $text
786 789 \s?
787 790 (?:\(([^)]+?)\)(?="))? # $title
788 791 ":
789 792 ([\w\/]\S+?) # $url
790 793 (\/)? # $slash
791 794 ([^\w\=\/;\(\)]*?) # $post
792 795 (?=<|\s|$)
793 796 /x
794 797 #"
795 798 def inline_textile_link( text )
796 799 text.gsub!( LINK_RE ) do |m|
797 800 pre,atts,text,title,url,slash,post = $~[1..7]
798 801
799 802 url, url_title = check_refs( url )
800 803 title ||= url_title
801 804
802 805 # Idea below : an URL with unbalanced parethesis and
803 806 # ending by ')' is put into external parenthesis
804 807 if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
805 808 url=url[0..-2] # discard closing parenth from url
806 809 post = ")"+post # add closing parenth to post
807 810 end
808 811 atts = pba( atts )
809 812 atts = " href=\"#{ url }#{ slash }\"#{ atts }"
810 813 atts << " title=\"#{ title }\"" if title
811 814 atts = shelve( atts ) if atts
812 815
813 816 external = (url =~ /^https?:\/\//) ? ' class="external"' : ''
814 817
815 818 "#{ pre }<a#{ atts }#{ external }>#{ text }</a>#{ post }"
816 819 end
817 820 end
818 821
819 822 MARKDOWN_REFLINK_RE = /
820 823 \[([^\[\]]+)\] # $text
821 824 [ ]? # opt. space
822 825 (?:\n[ ]*)? # one optional newline followed by spaces
823 826 \[(.*?)\] # $id
824 827 /x
825 828
826 829 def inline_markdown_reflink( text )
827 830 text.gsub!( MARKDOWN_REFLINK_RE ) do |m|
828 831 text, id = $~[1..2]
829 832
830 833 if id.empty?
831 834 url, title = check_refs( text )
832 835 else
833 836 url, title = check_refs( id )
834 837 end
835 838
836 839 atts = " href=\"#{ url }\""
837 840 atts << " title=\"#{ title }\"" if title
838 841 atts = shelve( atts )
839 842
840 843 "<a#{ atts }>#{ text }</a>"
841 844 end
842 845 end
843 846
844 847 MARKDOWN_LINK_RE = /
845 848 \[([^\[\]]+)\] # $text
846 849 \( # open paren
847 850 [ \t]* # opt space
848 851 <?(.+?)>? # $href
849 852 [ \t]* # opt space
850 853 (?: # whole title
851 854 (['"]) # $quote
852 855 (.*?) # $title
853 856 \3 # matching quote
854 857 )? # title is optional
855 858 \)
856 859 /x
857 860
858 861 def inline_markdown_link( text )
859 862 text.gsub!( MARKDOWN_LINK_RE ) do |m|
860 863 text, url, quote, title = $~[1..4]
861 864
862 865 atts = " href=\"#{ url }\""
863 866 atts << " title=\"#{ title }\"" if title
864 867 atts = shelve( atts )
865 868
866 869 "<a#{ atts }>#{ text }</a>"
867 870 end
868 871 end
869 872
870 873 TEXTILE_REFS_RE = /(^ *)\[([^\[\n]+?)\](#{HYPERLINK})(?=\s|$)/
871 874 MARKDOWN_REFS_RE = /(^ *)\[([^\n]+?)\]:\s+<?(#{HYPERLINK})>?(?:\s+"((?:[^"]|\\")+)")?(?=\s|$)/m
872 875
873 876 def refs( text )
874 877 @rules.each do |rule_name|
875 878 method( rule_name ).call( text ) if rule_name.to_s.match /^refs_/
876 879 end
877 880 end
878 881
879 882 def refs_textile( text )
880 883 text.gsub!( TEXTILE_REFS_RE ) do |m|
881 884 flag, url = $~[2..3]
882 885 @urlrefs[flag.downcase] = [url, nil]
883 886 nil
884 887 end
885 888 end
886 889
887 890 def refs_markdown( text )
888 891 text.gsub!( MARKDOWN_REFS_RE ) do |m|
889 892 flag, url = $~[2..3]
890 893 title = $~[6]
891 894 @urlrefs[flag.downcase] = [url, title]
892 895 nil
893 896 end
894 897 end
895 898
896 899 def check_refs( text )
897 900 ret = @urlrefs[text.downcase] if text
898 901 ret || [text, nil]
899 902 end
900 903
901 904 IMAGE_RE = /
902 905 (<p>|.|^) # start of line?
903 906 \! # opening
904 907 (\<|\=|\>)? # optional alignment atts
905 908 (#{C}) # optional style,class atts
906 909 (?:\. )? # optional dot-space
907 910 ([^\s(!]+?) # presume this is the src
908 911 \s? # optional space
909 912 (?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title
910 913 \! # closing
911 914 (?::#{ HYPERLINK })? # optional href
912 915 /x
913 916
914 917 def inline_textile_image( text )
915 918 text.gsub!( IMAGE_RE ) do |m|
916 919 stln,algn,atts,url,title,href,href_a1,href_a2 = $~[1..8]
920 htmlesc title
917 921 atts = pba( atts )
918 922 atts = " src=\"#{ url }\"#{ atts }"
919 923 atts << " title=\"#{ title }\"" if title
920 924 atts << " alt=\"#{ title }\""
921 925 # size = @getimagesize($url);
922 926 # if($size) $atts.= " $size[3]";
923 927
924 928 href, alt_title = check_refs( href ) if href
925 929 url, url_title = check_refs( url )
926 930
927 931 out = ''
928 932 out << "<a#{ shelve( " href=\"#{ href }\"" ) }>" if href
929 933 out << "<img#{ shelve( atts ) } />"
930 934 out << "</a>#{ href_a1 }#{ href_a2 }" if href
931 935
932 936 if algn
933 937 algn = h_align( algn )
934 938 if stln == "<p>"
935 939 out = "<p style=\"float:#{ algn }\">#{ out }"
936 940 else
937 941 out = "#{ stln }<div style=\"float:#{ algn }\">#{ out }</div>"
938 942 end
939 943 else
940 944 out = stln + out
941 945 end
942 946
943 947 out
944 948 end
945 949 end
946 950
947 951 def shelve( val )
948 952 @shelf << val
949 953 " :redsh##{ @shelf.length }:"
950 954 end
951 955
952 956 def retrieve( text )
953 957 @shelf.each_with_index do |r, i|
954 958 text.gsub!( " :redsh##{ i + 1 }:", r )
955 959 end
956 960 end
957 961
958 962 def incoming_entities( text )
959 963 ## turn any incoming ampersands into a dummy character for now.
960 964 ## This uses a negative lookahead for alphanumerics followed by a semicolon,
961 965 ## implying an incoming html entity, to be skipped
962 966
963 967 text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" )
964 968 end
965 969
966 970 def no_textile( text )
967 971 text.gsub!( /(^|\s)==([^=]+.*?)==(\s|$)?/,
968 972 '\1<notextile>\2</notextile>\3' )
969 973 text.gsub!( /^ *==([^=]+.*?)==/m,
970 974 '\1<notextile>\2</notextile>\3' )
971 975 end
972 976
973 977 def clean_white_space( text )
974 978 # normalize line breaks
975 979 text.gsub!( /\r\n/, "\n" )
976 980 text.gsub!( /\r/, "\n" )
977 981 text.gsub!( /\t/, ' ' )
978 982 text.gsub!( /^ +$/, '' )
979 983 text.gsub!( /\n{3,}/, "\n\n" )
980 984 text.gsub!( /"$/, "\" " )
981 985
982 986 # if entire document is indented, flush
983 987 # to the left side
984 988 flush_left text
985 989 end
986 990
987 991 def flush_left( text )
988 992 indt = 0
989 993 if text =~ /^ /
990 994 while text !~ /^ {#{indt}}\S/
991 995 indt += 1
992 996 end unless text.empty?
993 997 if indt.nonzero?
994 998 text.gsub!( /^ {#{indt}}/, '' )
995 999 end
996 1000 end
997 1001 end
998 1002
999 1003 def footnote_ref( text )
1000 1004 text.gsub!( /\b\[([0-9]+?)\](\s)?/,
1001 1005 '<sup><a href="#fn\1">\1</a></sup>\2' )
1002 1006 end
1003 1007
1004 1008 OFFTAGS = /(code|pre|kbd|notextile)/
1005 1009 OFFTAG_MATCH = /(?:(<\/#{ OFFTAGS }>)|(<#{ OFFTAGS }[^>]*>))(.*?)(?=<\/?#{ OFFTAGS }|\Z)/mi
1006 1010 OFFTAG_OPEN = /<#{ OFFTAGS }/
1007 1011 OFFTAG_CLOSE = /<\/?#{ OFFTAGS }/
1008 1012 HASTAG_MATCH = /(<\/?\w[^\n]*?>)/m
1009 1013 ALLTAG_MATCH = /(<\/?\w[^\n]*?>)|.*?(?=<\/?\w[^\n]*?>|$)/m
1010 1014
1011 1015 def glyphs_textile( text, level = 0 )
1012 1016 if text !~ HASTAG_MATCH
1013 1017 pgl text
1014 1018 footnote_ref text
1015 1019 else
1016 1020 codepre = 0
1017 1021 text.gsub!( ALLTAG_MATCH ) do |line|
1018 1022 ## matches are off if we're between <code>, <pre> etc.
1019 1023 if $1
1020 1024 if line =~ OFFTAG_OPEN
1021 1025 codepre += 1
1022 1026 elsif line =~ OFFTAG_CLOSE
1023 1027 codepre -= 1
1024 1028 codepre = 0 if codepre < 0
1025 1029 end
1026 1030 elsif codepre.zero?
1027 1031 glyphs_textile( line, level + 1 )
1028 1032 else
1029 1033 htmlesc( line, :NoQuotes )
1030 1034 end
1031 1035 # p [level, codepre, line]
1032 1036
1033 1037 line
1034 1038 end
1035 1039 end
1036 1040 end
1037 1041
1038 1042 def rip_offtags( text )
1039 1043 if text =~ /<.*>/
1040 1044 ## strip and encode <pre> content
1041 1045 codepre, used_offtags = 0, {}
1042 1046 text.gsub!( OFFTAG_MATCH ) do |line|
1043 1047 if $3
1044 1048 offtag, aftertag = $4, $5
1045 1049 codepre += 1
1046 1050 used_offtags[offtag] = true
1047 1051 if codepre - used_offtags.length > 0
1048 1052 htmlesc( line, :NoQuotes )
1049 1053 @pre_list.last << line
1050 1054 line = ""
1051 1055 else
1052 1056 htmlesc( aftertag, :NoQuotes ) if aftertag
1053 1057 line = "<redpre##{ @pre_list.length }>"
1054 1058 $3.match(/<#{ OFFTAGS }([^>]*)>/)
1055 1059 tag = $1
1056 1060 $2.to_s.match(/(class\=\S+)/i)
1057 1061 tag << " #{$1}" if $1
1058 1062 @pre_list << "<#{ tag }>#{ aftertag }"
1059 1063 end
1060 1064 elsif $1 and codepre > 0
1061 1065 if codepre - used_offtags.length > 0
1062 1066 htmlesc( line, :NoQuotes )
1063 1067 @pre_list.last << line
1064 1068 line = ""
1065 1069 end
1066 1070 codepre -= 1 unless codepre.zero?
1067 1071 used_offtags = {} if codepre.zero?
1068 1072 end
1069 1073 line
1070 1074 end
1071 1075 end
1072 1076 text
1073 1077 end
1074 1078
1075 1079 def smooth_offtags( text )
1076 1080 unless @pre_list.empty?
1077 1081 ## replace <pre> content
1078 1082 text.gsub!( /<redpre#(\d+)>/ ) { @pre_list[$1.to_i] }
1079 1083 end
1080 1084 end
1081 1085
1082 1086 def inline( text )
1083 1087 [/^inline_/, /^glyphs_/].each do |meth_re|
1084 1088 @rules.each do |rule_name|
1085 1089 method( rule_name ).call( text ) if rule_name.to_s.match( meth_re )
1086 1090 end
1087 1091 end
1088 1092 end
1089 1093
1090 1094 def h_align( text )
1091 1095 H_ALGN_VALS[text]
1092 1096 end
1093 1097
1094 1098 def v_align( text )
1095 1099 V_ALGN_VALS[text]
1096 1100 end
1097 1101
1098 1102 def textile_popup_help( name, windowW, windowH )
1099 1103 ' <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 />'
1100 1104 end
1101 1105
1102 1106 # HTML cleansing stuff
1103 1107 BASIC_TAGS = {
1104 1108 'a' => ['href', 'title'],
1105 1109 'img' => ['src', 'alt', 'title'],
1106 1110 'br' => [],
1107 1111 'i' => nil,
1108 1112 'u' => nil,
1109 1113 'b' => nil,
1110 1114 'pre' => nil,
1111 1115 'kbd' => nil,
1112 1116 'code' => ['lang'],
1113 1117 'cite' => nil,
1114 1118 'strong' => nil,
1115 1119 'em' => nil,
1116 1120 'ins' => nil,
1117 1121 'sup' => nil,
1118 1122 'sub' => nil,
1119 1123 'del' => nil,
1120 1124 'table' => nil,
1121 1125 'tr' => nil,
1122 1126 'td' => ['colspan', 'rowspan'],
1123 1127 'th' => nil,
1124 1128 'ol' => nil,
1125 1129 'ul' => nil,
1126 1130 'li' => nil,
1127 1131 'p' => nil,
1128 1132 'h1' => nil,
1129 1133 'h2' => nil,
1130 1134 'h3' => nil,
1131 1135 'h4' => nil,
1132 1136 'h5' => nil,
1133 1137 'h6' => nil,
1134 1138 'blockquote' => ['cite']
1135 1139 }
1136 1140
1137 1141 def clean_html( text, tags = BASIC_TAGS )
1138 1142 text.gsub!( /<!\[CDATA\[/, '' )
1139 1143 text.gsub!( /<(\/*)(\w+)([^>]*)>/ ) do
1140 1144 raw = $~
1141 1145 tag = raw[2].downcase
1142 1146 if tags.has_key? tag
1143 1147 pcs = [tag]
1144 1148 tags[tag].each do |prop|
1145 1149 ['"', "'", ''].each do |q|
1146 1150 q2 = ( q != '' ? q : '\s' )
1147 1151 if raw[3] =~ /#{prop}\s*=\s*#{q}([^#{q2}]+)#{q}/i
1148 1152 attrv = $1
1149 1153 next if prop == 'src' and attrv =~ %r{^(?!http)\w+:}
1150 1154 pcs << "#{prop}=\"#{$1.gsub('"', '\\"')}\""
1151 1155 break
1152 1156 end
1153 1157 end
1154 1158 end if tags[tag]
1155 1159 "<#{raw[1]}#{pcs.join " "}>"
1156 1160 else
1157 1161 " "
1158 1162 end
1159 1163 end
1160 1164 end
1161 1165
1162 1166 ALLOWED_TAGS = %w(redpre pre code notextile)
1163 1167
1164 1168 def escape_html_tags(text)
1165 1169 text.gsub!(%r{<(\/?([!\w]+)[^<>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "&lt;#{$1}#{'&gt;' unless $3.blank?}" }
1166 1170 end
1167 1171 end
1168 1172
@@ -1,431 +1,433
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../../test_helper'
19 19
20 20 class ApplicationHelperTest < HelperTestCase
21 21 include ApplicationHelper
22 22 include ActionView::Helpers::TextHelper
23 23 fixtures :projects, :roles, :enabled_modules, :users,
24 24 :repositories, :changesets,
25 25 :trackers, :issue_statuses, :issues, :versions, :documents,
26 26 :wikis, :wiki_pages, :wiki_contents,
27 27 :boards, :messages,
28 28 :attachments
29 29
30 30 def setup
31 31 super
32 32 end
33 33
34 34 def test_auto_links
35 35 to_test = {
36 36 'http://foo.bar' => '<a class="external" href="http://foo.bar">http://foo.bar</a>',
37 37 'http://foo.bar/~user' => '<a class="external" href="http://foo.bar/~user">http://foo.bar/~user</a>',
38 38 'http://foo.bar.' => '<a class="external" href="http://foo.bar">http://foo.bar</a>.',
39 39 'https://foo.bar.' => '<a class="external" href="https://foo.bar">https://foo.bar</a>.',
40 40 'This is a link: http://foo.bar.' => 'This is a link: <a class="external" href="http://foo.bar">http://foo.bar</a>.',
41 41 'A link (eg. http://foo.bar).' => 'A link (eg. <a class="external" href="http://foo.bar">http://foo.bar</a>).',
42 42 '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>.',
43 43 'http://www.foo.bar/Test_(foobar)' => '<a class="external" href="http://www.foo.bar/Test_(foobar)">http://www.foo.bar/Test_(foobar)</a>',
44 44 '(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>)',
45 45 '(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>)',
46 46 '(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>).',
47 47 '(see "inline link":http://www.foo.bar/Test_(foobar))' => '(see <a href="http://www.foo.bar/Test_(foobar)" class="external">inline link</a>)',
48 48 '(see "inline link":http://www.foo.bar/Test)' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>)',
49 49 '(see "inline link":http://www.foo.bar/Test).' => '(see <a href="http://www.foo.bar/Test" class="external">inline link</a>).',
50 50 'www.foo.bar' => '<a class="external" href="http://www.foo.bar">www.foo.bar</a>',
51 51 '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>',
52 52 'http://foo.bar/page#125' => '<a class="external" href="http://foo.bar/page#125">http://foo.bar/page#125</a>',
53 53 'http://foo@www.bar.com' => '<a class="external" href="http://foo@www.bar.com">http://foo@www.bar.com</a>',
54 54 'http://foo:bar@www.bar.com' => '<a class="external" href="http://foo:bar@www.bar.com">http://foo:bar@www.bar.com</a>',
55 55 'ftp://foo.bar' => '<a class="external" href="ftp://foo.bar">ftp://foo.bar</a>',
56 56 'ftps://foo.bar' => '<a class="external" href="ftps://foo.bar">ftps://foo.bar</a>',
57 57 'sftp://foo.bar' => '<a class="external" href="sftp://foo.bar">sftp://foo.bar</a>',
58 58 }
59 59 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
60 60 end
61 61
62 62 def test_auto_mailto
63 63 assert_equal '<p><a href="mailto:test@foo.bar" class="email">test@foo.bar</a></p>',
64 64 textilizable('test@foo.bar')
65 65 end
66 66
67 67 def test_inline_images
68 68 to_test = {
69 69 '!http://foo.bar/image.jpg!' => '<img src="http://foo.bar/image.jpg" alt="" />',
70 70 'floating !>http://foo.bar/image.jpg!' => 'floating <div style="float:right"><img src="http://foo.bar/image.jpg" alt="" /></div>',
71 71 'with class !(some-class)http://foo.bar/image.jpg!' => 'with class <img src="http://foo.bar/image.jpg" class="some-class" alt="" />',
72 72 'with style !{width:100px;height100px}http://foo.bar/image.jpg!' => 'with style <img src="http://foo.bar/image.jpg" style="width:100px;height100px;" alt="" />',
73 '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" />',
74 '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;" />',
73 75 }
74 76 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
75 77 end
76 78
77 79 def test_attached_images
78 80 to_test = {
79 81 'Inline image: !logo.gif!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />',
80 82 'Inline image: !logo.GIF!' => 'Inline image: <img src="/attachments/download/3" title="This is a logo" alt="This is a logo" />'
81 83 }
82 84 attachments = Attachment.find(:all)
83 85 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text, :attachments => attachments) }
84 86 end
85 87
86 88 def test_textile_external_links
87 89 to_test = {
88 90 'This is a "link":http://foo.bar' => 'This is a <a href="http://foo.bar" class="external">link</a>',
89 91 'This is an intern "link":/foo/bar' => 'This is an intern <a href="/foo/bar">link</a>',
90 92 '"link (Link title)":http://foo.bar' => '<a href="http://foo.bar" title="Link title" class="external">link</a>',
91 93 "This is not a \"Link\":\n\nAnother paragraph" => "This is not a \"Link\":</p>\n\n\n\t<p>Another paragraph",
92 94 # no multiline link text
93 95 "This is a double quote \"on the first line\nand another on a second line\":test" => "This is a double quote \"on the first line<br />\nand another on a second line\":test"
94 96 }
95 97 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
96 98 end
97 99
98 100 def test_redmine_links
99 101 issue_link = link_to('#3', {:controller => 'issues', :action => 'show', :id => 3},
100 102 :class => 'issue', :title => 'Error 281 when updating a recipe (New)')
101 103
102 104 changeset_link = link_to('r1', {:controller => 'repositories', :action => 'revision', :id => 'ecookbook', :rev => 1},
103 105 :class => 'changeset', :title => 'My very first commit')
104 106
105 107 document_link = link_to('Test document', {:controller => 'documents', :action => 'show', :id => 1},
106 108 :class => 'document')
107 109
108 110 version_link = link_to('1.0', {:controller => 'versions', :action => 'show', :id => 2},
109 111 :class => 'version')
110 112
111 113 message_url = {:controller => 'messages', :action => 'show', :board_id => 1, :id => 4}
112 114
113 115 source_url = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file']}
114 116 source_url_with_ext = {:controller => 'repositories', :action => 'entry', :id => 'ecookbook', :path => ['some', 'file.ext']}
115 117
116 118 to_test = {
117 119 # tickets
118 120 '#3, #3 and #3.' => "#{issue_link}, #{issue_link} and #{issue_link}.",
119 121 # changesets
120 122 'r1' => changeset_link,
121 123 # documents
122 124 'document#1' => document_link,
123 125 'document:"Test document"' => document_link,
124 126 # versions
125 127 'version#2' => version_link,
126 128 'version:1.0' => version_link,
127 129 'version:"1.0"' => version_link,
128 130 # source
129 131 'source:/some/file' => link_to('source:/some/file', source_url, :class => 'source'),
130 132 'source:/some/file.' => link_to('source:/some/file', source_url, :class => 'source') + ".",
131 133 'source:/some/file.ext.' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
132 134 'source:/some/file. ' => link_to('source:/some/file', source_url, :class => 'source') + ".",
133 135 'source:/some/file.ext. ' => link_to('source:/some/file.ext', source_url_with_ext, :class => 'source') + ".",
134 136 'source:/some/file, ' => link_to('source:/some/file', source_url, :class => 'source') + ",",
135 137 'source:/some/file@52' => link_to('source:/some/file@52', source_url.merge(:rev => 52), :class => 'source'),
136 138 'source:/some/file.ext@52' => link_to('source:/some/file.ext@52', source_url_with_ext.merge(:rev => 52), :class => 'source'),
137 139 'source:/some/file#L110' => link_to('source:/some/file#L110', source_url.merge(:anchor => 'L110'), :class => 'source'),
138 140 'source:/some/file.ext#L110' => link_to('source:/some/file.ext#L110', source_url_with_ext.merge(:anchor => 'L110'), :class => 'source'),
139 141 'source:/some/file@52#L110' => link_to('source:/some/file@52#L110', source_url.merge(:rev => 52, :anchor => 'L110'), :class => 'source'),
140 142 'export:/some/file' => link_to('export:/some/file', source_url.merge(:format => 'raw'), :class => 'source download'),
141 143 # message
142 144 'message#4' => link_to('Post 2', message_url, :class => 'message'),
143 145 'message#5' => link_to('RE: post 2', message_url.merge(:anchor => 'message-5'), :class => 'message'),
144 146 # escaping
145 147 '!#3.' => '#3.',
146 148 '!r1' => 'r1',
147 149 '!document#1' => 'document#1',
148 150 '!document:"Test document"' => 'document:"Test document"',
149 151 '!version#2' => 'version#2',
150 152 '!version:1.0' => 'version:1.0',
151 153 '!version:"1.0"' => 'version:"1.0"',
152 154 '!source:/some/file' => 'source:/some/file',
153 155 # invalid expressions
154 156 'source:' => 'source:',
155 157 # url hash
156 158 "http://foo.bar/FAQ#3" => '<a class="external" href="http://foo.bar/FAQ#3">http://foo.bar/FAQ#3</a>',
157 159 }
158 160 @project = Project.find(1)
159 161 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
160 162 end
161 163
162 164 def test_wiki_links
163 165 to_test = {
164 166 '[[CookBook documentation]]' => '<a href="/wiki/ecookbook/CookBook_documentation" class="wiki-page">CookBook documentation</a>',
165 167 '[[Another page|Page]]' => '<a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a>',
166 168 # link with anchor
167 169 '[[CookBook documentation#One-section]]' => '<a href="/wiki/ecookbook/CookBook_documentation#One-section" class="wiki-page">CookBook documentation</a>',
168 170 '[[Another page#anchor|Page]]' => '<a href="/wiki/ecookbook/Another_page#anchor" class="wiki-page">Page</a>',
169 171 # page that doesn't exist
170 172 '[[Unknown page]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">Unknown page</a>',
171 173 '[[Unknown page|404]]' => '<a href="/wiki/ecookbook/Unknown_page" class="wiki-page new">404</a>',
172 174 # link to another project wiki
173 175 '[[onlinestore:]]' => '<a href="/wiki/onlinestore/" class="wiki-page">onlinestore</a>',
174 176 '[[onlinestore:|Wiki]]' => '<a href="/wiki/onlinestore/" class="wiki-page">Wiki</a>',
175 177 '[[onlinestore:Start page]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Start page</a>',
176 178 '[[onlinestore:Start page|Text]]' => '<a href="/wiki/onlinestore/Start_page" class="wiki-page">Text</a>',
177 179 '[[onlinestore:Unknown page]]' => '<a href="/wiki/onlinestore/Unknown_page" class="wiki-page new">Unknown page</a>',
178 180 # striked through link
179 181 '-[[Another page|Page]]-' => '<del><a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a></del>',
180 182 '-[[Another page|Page]] link-' => '<del><a href="/wiki/ecookbook/Another_page" class="wiki-page">Page</a> link</del>',
181 183 # escaping
182 184 '![[Another page|Page]]' => '[[Another page|Page]]',
183 185 }
184 186 @project = Project.find(1)
185 187 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
186 188 end
187 189
188 190 def test_html_tags
189 191 to_test = {
190 192 "<div>content</div>" => "<p>&lt;div&gt;content&lt;/div&gt;</p>",
191 193 "<div class=\"bold\">content</div>" => "<p>&lt;div class=\"bold\"&gt;content&lt;/div&gt;</p>",
192 194 "<script>some script;</script>" => "<p>&lt;script&gt;some script;&lt;/script&gt;</p>",
193 195 # do not escape pre/code tags
194 196 "<pre>\nline 1\nline2</pre>" => "<pre>\nline 1\nline2</pre>",
195 197 "<pre><code>\nline 1\nline2</code></pre>" => "<pre><code>\nline 1\nline2</code></pre>",
196 198 "<pre><div>content</div></pre>" => "<pre>&lt;div&gt;content&lt;/div&gt;</pre>",
197 199 "HTML comment: <!-- no comments -->" => "<p>HTML comment: &lt;!-- no comments --&gt;</p>",
198 200 "<!-- opening comment" => "<p>&lt;!-- opening comment</p>",
199 201 # remove attributes except class
200 202 "<pre class='foo'>some text</pre>" => "<pre class='foo'>some text</pre>",
201 203 "<pre onmouseover='alert(1)'>some text</pre>" => "<pre>some text</pre>",
202 204 }
203 205 to_test.each { |text, result| assert_equal result, textilizable(text) }
204 206 end
205 207
206 208 def test_allowed_html_tags
207 209 to_test = {
208 210 "<pre>preformatted text</pre>" => "<pre>preformatted text</pre>",
209 211 "<notextile>no *textile* formatting</notextile>" => "no *textile* formatting",
210 212 "<notextile>this is <tag>a tag</tag></notextile>" => "this is &lt;tag&gt;a tag&lt;/tag&gt;"
211 213 }
212 214 to_test.each { |text, result| assert_equal result, textilizable(text) }
213 215 end
214 216
215 217 def syntax_highlight
216 218 raw = <<-RAW
217 219 <pre><code class="ruby">
218 220 # Some ruby code here
219 221 </pre></code>
220 222 RAW
221 223
222 224 expected = <<-EXPECTED
223 225 <pre><code class="ruby CodeRay"><span class="no">1</span> <span class="c"># Some ruby code here</span>
224 226 </pre></code>
225 227 EXPECTED
226 228
227 229 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
228 230 end
229 231
230 232 def test_wiki_links_in_tables
231 233 to_test = {"|[[Page|Link title]]|[[Other Page|Other title]]|\n|Cell 21|[[Last page]]|" =>
232 234 '<tr><td><a href="/wiki/ecookbook/Page" class="wiki-page new">Link title</a></td>' +
233 235 '<td><a href="/wiki/ecookbook/Other_Page" class="wiki-page new">Other title</a></td>' +
234 236 '</tr><tr><td>Cell 21</td><td><a href="/wiki/ecookbook/Last_page" class="wiki-page new">Last page</a></td></tr>'
235 237 }
236 238 @project = Project.find(1)
237 239 to_test.each { |text, result| assert_equal "<table>#{result}</table>", textilizable(text).gsub(/[\t\n]/, '') }
238 240 end
239 241
240 242 def test_text_formatting
241 243 to_test = {'*_+bold, italic and underline+_*' => '<strong><em><ins>bold, italic and underline</ins></em></strong>',
242 244 '(_text within parentheses_)' => '(<em>text within parentheses</em>)'
243 245 }
244 246 to_test.each { |text, result| assert_equal "<p>#{result}</p>", textilizable(text) }
245 247 end
246 248
247 249 def test_wiki_horizontal_rule
248 250 assert_equal '<hr />', textilizable('---')
249 251 assert_equal '<p>Dashes: ---</p>', textilizable('Dashes: ---')
250 252 end
251 253
252 254 def test_acronym
253 255 assert_equal '<p>This is an acronym: <acronym title="American Civil Liberties Union">ACLU</acronym>.</p>',
254 256 textilizable('This is an acronym: ACLU(American Civil Liberties Union).')
255 257 end
256 258
257 259 def test_footnotes
258 260 raw = <<-RAW
259 261 This is some text[1].
260 262
261 263 fn1. This is the foot note
262 264 RAW
263 265
264 266 expected = <<-EXPECTED
265 267 <p>This is some text<sup><a href=\"#fn1\">1</a></sup>.</p>
266 268 <p id="fn1" class="footnote"><sup>1</sup> This is the foot note</p>
267 269 EXPECTED
268 270
269 271 assert_equal expected.gsub(%r{[\r\n\t]}, ''), textilizable(raw).gsub(%r{[\r\n\t]}, '')
270 272 end
271 273
272 274 def test_table_of_content
273 275 raw = <<-RAW
274 276 {{toc}}
275 277
276 278 h1. Title
277 279
278 280 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
279 281
280 282 h2. Subtitle
281 283
282 284 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
283 285
284 286 h2. Subtitle with %{color:red}red text%
285 287
286 288 h1. Another title
287 289
288 290 RAW
289 291
290 292 expected = '<ul class="toc">' +
291 293 '<li class="heading1"><a href="#Title">Title</a></li>' +
292 294 '<li class="heading2"><a href="#Subtitle">Subtitle</a></li>' +
293 295 '<li class="heading2"><a href="#Subtitle-with-red-text">Subtitle with red text</a></li>' +
294 296 '<li class="heading1"><a href="#Another-title">Another title</a></li>' +
295 297 '</ul>'
296 298
297 299 assert textilizable(raw).gsub("\n", "").include?(expected)
298 300 end
299 301
300 302 def test_blockquote
301 303 # orig raw text
302 304 raw = <<-RAW
303 305 John said:
304 306 > Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
305 307 > Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
306 308 > * Donec odio lorem,
307 309 > * sagittis ac,
308 310 > * malesuada in,
309 311 > * adipiscing eu, dolor.
310 312 >
311 313 > >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
312 314 > Proin a tellus. Nam vel neque.
313 315
314 316 He's right.
315 317 RAW
316 318
317 319 # expected html
318 320 expected = <<-EXPECTED
319 321 <p>John said:</p>
320 322 <blockquote>
321 323 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
322 324 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
323 325 <ul>
324 326 <li>Donec odio lorem,</li>
325 327 <li>sagittis ac,</li>
326 328 <li>malesuada in,</li>
327 329 <li>adipiscing eu, dolor.</li>
328 330 </ul>
329 331 <blockquote>
330 332 <p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
331 333 </blockquote>
332 334 <p>Proin a tellus. Nam vel neque.</p>
333 335 </blockquote>
334 336 <p>He's right.</p>
335 337 EXPECTED
336 338
337 339 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
338 340 end
339 341
340 342 def test_table
341 343 raw = <<-RAW
342 344 This is a table with empty cells:
343 345
344 346 |cell11|cell12||
345 347 |cell21||cell23|
346 348 |cell31|cell32|cell33|
347 349 RAW
348 350
349 351 expected = <<-EXPECTED
350 352 <p>This is a table with empty cells:</p>
351 353
352 354 <table>
353 355 <tr><td>cell11</td><td>cell12</td><td></td></tr>
354 356 <tr><td>cell21</td><td></td><td>cell23</td></tr>
355 357 <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
356 358 </table>
357 359 EXPECTED
358 360
359 361 assert_equal expected.gsub(%r{\s+}, ''), textilizable(raw).gsub(%r{\s+}, '')
360 362 end
361 363
362 364 def test_default_formatter
363 365 Setting.text_formatting = 'unknown'
364 366 text = 'a *link*: http://www.example.net/'
365 367 assert_equal '<p>a *link*: <a href="http://www.example.net/">http://www.example.net/</a></p>', textilizable(text)
366 368 Setting.text_formatting = 'textile'
367 369 end
368 370
369 371 def test_date_format_default
370 372 today = Date.today
371 373 Setting.date_format = ''
372 374 assert_equal l_date(today), format_date(today)
373 375 end
374 376
375 377 def test_date_format
376 378 today = Date.today
377 379 Setting.date_format = '%d %m %Y'
378 380 assert_equal today.strftime('%d %m %Y'), format_date(today)
379 381 end
380 382
381 383 def test_time_format_default
382 384 now = Time.now
383 385 Setting.date_format = ''
384 386 Setting.time_format = ''
385 387 assert_equal l_datetime(now), format_time(now)
386 388 assert_equal l_time(now), format_time(now, false)
387 389 end
388 390
389 391 def test_time_format
390 392 now = Time.now
391 393 Setting.date_format = '%d %m %Y'
392 394 Setting.time_format = '%H %M'
393 395 assert_equal now.strftime('%d %m %Y %H %M'), format_time(now)
394 396 assert_equal now.strftime('%H %M'), format_time(now, false)
395 397 end
396 398
397 399 def test_utc_time_format
398 400 now = Time.now.utc
399 401 Setting.date_format = '%d %m %Y'
400 402 Setting.time_format = '%H %M'
401 403 assert_equal Time.now.strftime('%d %m %Y %H %M'), format_time(now)
402 404 assert_equal Time.now.strftime('%H %M'), format_time(now, false)
403 405 end
404 406
405 407 def test_due_date_distance_in_words
406 408 to_test = { Date.today => 'Due in 0 days',
407 409 Date.today + 1 => 'Due in 1 day',
408 410 Date.today + 100 => 'Due in 100 days',
409 411 Date.today + 20000 => 'Due in 20000 days',
410 412 Date.today - 1 => '1 day late',
411 413 Date.today - 100 => '100 days late',
412 414 Date.today - 20000 => '20000 days late',
413 415 }
414 416 to_test.each do |date, expected|
415 417 assert_equal expected, due_date_distance_in_words(date)
416 418 end
417 419 end
418 420
419 421 def test_avatar
420 422 # turn on avatars
421 423 Setting.gravatar_enabled = '1'
422 424 assert avatar(User.find_by_mail('jsmith@somenet.foo')).include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
423 425 assert avatar('jsmith <jsmith@somenet.foo>').include?(Digest::MD5.hexdigest('jsmith@somenet.foo'))
424 426 assert_nil avatar('jsmith')
425 427 assert_nil avatar(nil)
426 428
427 429 # turn off avatars
428 430 Setting.gravatar_enabled = '0'
429 431 assert_nil avatar(User.find_by_mail('jsmith@somenet.foo'))
430 432 end
431 433 end
General Comments 0
You need to be logged in to leave comments. Login now