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