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