##// END OF EJS Templates
Reverts r14812 (#6969)....
Jean-Philippe Lang -
r14481:868d949f47c2
parent child
Show More
@@ -1,1208 +1,1208
1 1 # vim:ts=4:sw=4:
2 2 # = RedCloth - Textile and Markdown Hybrid for Ruby
3 3 #
4 4 # Homepage:: http://whytheluckystiff.net/ruby/redcloth/
5 5 # Author:: why the lucky stiff (http://whytheluckystiff.net/)
6 6 # Copyright:: (cc) 2004 why the lucky stiff (and his puppet organizations.)
7 7 # License:: BSD
8 8 #
9 9 # (see http://hobix.com/textile/ for a Textile Reference.)
10 10 #
11 11 # Based on (and also inspired by) both:
12 12 #
13 13 # PyTextile: http://diveintomark.org/projects/textile/textile.py.txt
14 14 # Textism for PHP: http://www.textism.com/tools/textile/
15 15 #
16 16 #
17 17
18 18 # = RedCloth
19 19 #
20 20 # RedCloth is a Ruby library for converting Textile and/or Markdown
21 21 # into HTML. You can use either format, intermingled or separately.
22 22 # You can also extend RedCloth to honor your own custom text stylings.
23 23 #
24 24 # RedCloth users are encouraged to use Textile if they are generating
25 25 # HTML and to use Markdown if others will be viewing the plain text.
26 26 #
27 27 # == What is Textile?
28 28 #
29 29 # Textile is a simple formatting style for text
30 30 # documents, loosely based on some HTML conventions.
31 31 #
32 32 # == Sample Textile Text
33 33 #
34 34 # h2. This is a title
35 35 #
36 36 # h3. This is a subhead
37 37 #
38 38 # This is a bit of paragraph.
39 39 #
40 40 # bq. This is a blockquote.
41 41 #
42 42 # = Writing Textile
43 43 #
44 44 # A Textile document consists of paragraphs. Paragraphs
45 45 # can be specially formatted by adding a small instruction
46 46 # to the beginning of the paragraph.
47 47 #
48 48 # h[n]. Header of size [n].
49 49 # bq. Blockquote.
50 50 # # Numeric list.
51 51 # * Bulleted list.
52 52 #
53 53 # == Quick Phrase Modifiers
54 54 #
55 55 # Quick phrase modifiers are also included, to allow formatting
56 56 # of small portions of text within a paragraph.
57 57 #
58 58 # \_emphasis\_
59 59 # \_\_italicized\_\_
60 60 # \*strong\*
61 61 # \*\*bold\*\*
62 62 # ??citation??
63 63 # -deleted text-
64 64 # +inserted text+
65 65 # ^superscript^
66 66 # ~subscript~
67 67 # @code@
68 68 # %(classname)span%
69 69 #
70 70 # ==notextile== (leave text alone)
71 71 #
72 72 # == Links
73 73 #
74 74 # To make a hypertext link, put the link text in "quotation
75 75 # marks" followed immediately by a colon and the URL of the link.
76 76 #
77 77 # Optional: text in (parentheses) following the link text,
78 78 # but before the closing quotation mark, will become a Title
79 79 # attribute for the link, visible as a tool tip when a cursor is above it.
80 80 #
81 81 # Example:
82 82 #
83 83 # "This is a link (This is a title) ":http://www.textism.com
84 84 #
85 85 # Will become:
86 86 #
87 87 # <a href="http://www.textism.com" title="This is a title">This is a link</a>
88 88 #
89 89 # == Images
90 90 #
91 91 # To insert an image, put the URL for the image inside exclamation marks.
92 92 #
93 93 # Optional: text that immediately follows the URL in (parentheses) will
94 94 # be used as the Alt text for the image. Images on the web should always
95 95 # have descriptive Alt text for the benefit of readers using non-graphical
96 96 # browsers.
97 97 #
98 98 # Optional: place a colon followed by a URL immediately after the
99 99 # closing ! to make the image into a link.
100 100 #
101 101 # Example:
102 102 #
103 103 # !http://www.textism.com/common/textist.gif(Textist)!
104 104 #
105 105 # Will become:
106 106 #
107 107 # <img src="http://www.textism.com/common/textist.gif" alt="Textist" />
108 108 #
109 109 # With a link:
110 110 #
111 111 # !/common/textist.gif(Textist)!:http://textism.com
112 112 #
113 113 # Will become:
114 114 #
115 115 # <a href="http://textism.com"><img src="/common/textist.gif" alt="Textist" /></a>
116 116 #
117 117 # == Defining Acronyms
118 118 #
119 119 # HTML allows authors to define acronyms via the tag. The definition appears as a
120 120 # tool tip when a cursor hovers over the acronym. A crucial aid to clear writing,
121 121 # this should be used at least once for each acronym in documents where they appear.
122 122 #
123 123 # To quickly define an acronym in Textile, place the full text in (parentheses)
124 124 # immediately following the acronym.
125 125 #
126 126 # Example:
127 127 #
128 128 # ACLU(American Civil Liberties Union)
129 129 #
130 130 # Will become:
131 131 #
132 132 # <abbr title="American Civil Liberties Union">ACLU</abbr>
133 133 #
134 134 # == Adding Tables
135 135 #
136 136 # In Textile, simple tables can be added by separating each column by
137 137 # a pipe.
138 138 #
139 139 # |a|simple|table|row|
140 140 # |And|Another|table|row|
141 141 #
142 142 # Attributes are defined by style definitions in parentheses.
143 143 #
144 144 # table(border:1px solid black).
145 145 # (background:#ddd;color:red). |{}| | | |
146 146 #
147 147 # == Using RedCloth
148 148 #
149 149 # RedCloth is simply an extension of the String class, which can handle
150 150 # Textile formatting. Use it like a String and output HTML with its
151 151 # RedCloth#to_html method.
152 152 #
153 153 # doc = RedCloth.new "
154 154 #
155 155 # h2. Test document
156 156 #
157 157 # Just a simple test."
158 158 #
159 159 # puts doc.to_html
160 160 #
161 161 # By default, RedCloth uses both Textile and Markdown formatting, with
162 162 # Textile formatting taking precedence. If you want to turn off Markdown
163 163 # formatting, to boost speed and limit the processor:
164 164 #
165 165 # class RedCloth::Textile.new( str )
166 166
167 167 class RedCloth3 < String
168 168
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 = [: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 # need to do this before #hard_break and #blocks
300 300 block_textile_quotes text unless @lite_mode
301 301 hard_break text
302 302 unless @lite_mode
303 303 refs 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 = '(?:\[[a-z\-_]+\])'
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_JOIN = QTAGS.map {|rc, ht, rtype| Regexp::quote rc}.join('|')
377 377
378 378 QTAGS.collect! do |rc, ht, rtype|
379 379 rcq = Regexp::quote rc
380 380 re =
381 381 case rtype
382 382 when :limit
383 383 /(^|[>\s\(]) # sta
384 384 (?!\-\-)
385 385 (#{QTAGS_JOIN}|) # oqs
386 386 (#{rcq}) # qtag
387 387 ([[:word:]]|[^\s].*?[^\s]) # content
388 388 (?!\-\-)
389 389 #{rcq}
390 390 (#{QTAGS_JOIN}|) # oqa
391 391 (?=[[:punct:]]|<|\s|\)|$)/x
392 392 else
393 393 /(#{rcq})
394 394 (#{C})
395 395 (?::(\S+))?
396 396 ([[:word:]]|[^\s\-].*?[^\s\-])
397 397 #{rcq}/xm
398 398 end
399 399 [rc, ht, re, rtype]
400 400 end
401 401
402 402 # Elements to handle
403 403 GLYPHS = [
404 404 # [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1&#8217;\2' ], # single closing
405 405 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)\'/, '\1&#8217;' ], # single closing
406 406 # [ /\'(?=[#{PUNCT_Q}]*(s\b|[\s#{PUNCT_NOQ}]))/, '&#8217;' ], # single closing
407 407 # [ /\'/, '&#8216;' ], # single opening
408 408 # [ /</, '&lt;' ], # less-than
409 409 # [ />/, '&gt;' ], # greater-than
410 410 # [ /([^\s\[{(])?"(\s|:|$)/, '\1&#8221;\2' ], # double closing
411 411 # [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)"/, '\1&#8221;' ], # double closing
412 412 # [ /"(?=[#{PUNCT_Q}]*[\s#{PUNCT_NOQ}])/, '&#8221;' ], # double closing
413 413 # [ /"/, '&#8220;' ], # double opening
414 414 # [ /\b( )?\.{3}/, '\1&#8230;' ], # ellipsis
415 415 # [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '<acronym title="\2">\1</acronym>' ], # 3+ uppercase acronym
416 416 # [ /(^|[^"][>\s])([A-Z][A-Z0-9 ]+[A-Z0-9])([^<A-Za-z0-9]|$)/, '\1<span class="caps">\2</span>\3', :no_span_caps ], # 3+ uppercase caps
417 417 # [ /(\.\s)?\s?--\s?/, '\1&#8212;' ], # em dash
418 418 # [ /\s->\s/, ' &rarr; ' ], # right arrow
419 419 # [ /\s-\s/, ' &#8211; ' ], # en dash
420 420 # [ /(\d+) ?x ?(\d+)/, '\1&#215;\2' ], # dimension sign
421 421 # [ /\b ?[(\[]TM[\])]/i, '&#8482;' ], # trademark
422 422 # [ /\b ?[(\[]R[\])]/i, '&#174;' ], # registered
423 423 # [ /\b ?[(\[]C[\])]/i, '&#169;' ] # copyright
424 424 ]
425 425
426 426 H_ALGN_VALS = {
427 427 '<' => 'left',
428 428 '=' => 'center',
429 429 '>' => 'right',
430 430 '<>' => 'justify'
431 431 }
432 432
433 433 V_ALGN_VALS = {
434 434 '^' => 'top',
435 435 '-' => 'middle',
436 436 '~' => 'bottom'
437 437 }
438 438
439 439 #
440 440 # Flexible HTML escaping
441 441 #
442 442 def htmlesc( str, mode=:Quotes )
443 443 if str
444 444 str.gsub!( '&', '&amp;' )
445 445 str.gsub!( '"', '&quot;' ) if mode != :NoQuotes
446 446 str.gsub!( "'", '&#039;' ) if mode == :Quotes
447 447 str.gsub!( '<', '&lt;')
448 448 str.gsub!( '>', '&gt;')
449 449 end
450 450 str
451 451 end
452 452
453 453 # Search and replace for Textile glyphs (quotes, dashes, other symbols)
454 454 def pgl( text )
455 455 #GLYPHS.each do |re, resub, tog|
456 456 # next if tog and method( tog ).call
457 457 # text.gsub! re, resub
458 458 #end
459 459 text.gsub!(/\b([A-Z][A-Z0-9]{1,})\b(?:[(]([^)]*)[)])/) do |m|
460 460 "<abbr title=\"#{htmlesc $2}\">#{$1}</abbr>"
461 461 end
462 462 end
463 463
464 464 # Parses Textile attribute lists and builds an HTML attribute string
465 465 def pba( text_in, element = "" )
466 466
467 467 return '' unless text_in
468 468
469 469 style = []
470 470 text = text_in.dup
471 471 if element == 'td'
472 472 colspan = $1 if text =~ /\\(\d+)/
473 473 rowspan = $1 if text =~ /\/(\d+)/
474 474 style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN
475 475 end
476 476
477 477 if text.sub!( /\{([^"}]*)\}/, '' ) && !filter_styles
478 478 sanitized = sanitize_styles($1)
479 479 style << "#{ sanitized };" unless sanitized.blank?
480 480 end
481 481
482 482 lang = $1 if
483 483 text.sub!( /\[([a-z\-_]+?)\]/, '' )
484 484
485 485 cls = $1 if
486 486 text.sub!( /\(([^()]+?)\)/, '' )
487 487
488 488 style << "padding-left:#{ $1.length }em;" if
489 489 text.sub!( /([(]+)/, '' )
490 490
491 491 style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' )
492 492
493 493 style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN
494 494
495 495 cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/
496 496
497 497 atts = ''
498 498 atts << " style=\"#{ style.join }\"" unless style.empty?
499 499 atts << " class=\"#{ cls }\"" unless cls.to_s.empty?
500 500 atts << " lang=\"#{ lang }\"" if lang
501 501 atts << " id=\"#{ id }\"" if id
502 502 atts << " colspan=\"#{ colspan }\"" if colspan
503 503 atts << " rowspan=\"#{ rowspan }\"" if rowspan
504 504
505 505 atts
506 506 end
507 507
508 508 STYLES_RE = /^(color|width|height|border|background|padding|margin|font|text|float)(-[a-z]+)*:\s*((\d+%?|\d+px|\d+(\.\d+)?em|#[0-9a-f]+|[a-z]+)\s*)+$/i
509 509
510 510 def sanitize_styles(str)
511 511 styles = str.split(";").map(&:strip)
512 512 styles.reject! do |style|
513 513 !style.match(STYLES_RE)
514 514 end
515 515 styles.join(";")
516 516 end
517 517
518 518 TABLE_RE = /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)(\n\n|\Z)/m
519 519
520 520 # Parses a Textile table block, building HTML from the result.
521 521 def block_textile_table( text )
522 522 text.gsub!( TABLE_RE ) do |matches|
523 523
524 524 tatts, fullrow = $~[1..2]
525 525 tatts = pba( tatts, 'table' )
526 526 tatts = shelve( tatts ) if tatts
527 527 rows = []
528 528 fullrow.gsub!(/([^|\s])\s*\n/, "\\1<br />")
529 529 fullrow.each_line do |row|
530 530 ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m
531 531 cells = []
532 532 # the regexp prevents wiki links with a | from being cut as cells
533 533 row.scan(/\|(_?#{S}#{A}#{C}\. ?)?((\[\[[^|\]]*\|[^|\]]*\]\]|[^|])*?)(?=\|)/) do |modifiers, cell|
534 534 ctyp = 'd'
535 535 ctyp = 'h' if modifiers && modifiers =~ /^_/
536 536
537 537 catts = nil
538 538 catts = pba( modifiers, 'td' ) if modifiers
539 539
540 540 catts = shelve( catts ) if catts
541 541 cells << "\t\t\t<t#{ ctyp }#{ catts }>#{ cell }</t#{ ctyp }>"
542 542 end
543 543 ratts = shelve( ratts ) if ratts
544 544 rows << "\t\t<tr#{ ratts }>\n#{ cells.join( "\n" ) }\n\t\t</tr>"
545 545 end
546 546 "\t<table#{ tatts }>\n#{ rows.join( "\n" ) }\n\t</table>\n\n"
547 547 end
548 548 end
549 549
550 550 LISTS_RE = /^([#*]+?#{C} .*?)$(?![^#*])/m
551 551 LISTS_CONTENT_RE = /^([#*]+)(#{A}#{C}) (.*)$/m
552 552
553 553 # Parses Textile lists and generates HTML
554 554 def block_textile_lists( text )
555 555 text.gsub!( LISTS_RE ) do |match|
556 556 lines = match.split( /\n/ )
557 557 last_line = -1
558 558 depth = []
559 559 lines.each_with_index do |line, line_id|
560 560 if line =~ LISTS_CONTENT_RE
561 561 tl,atts,content = $~[1..3]
562 562 if depth.last
563 563 if depth.last.length > tl.length
564 564 (depth.length - 1).downto(0) do |i|
565 565 break if depth[i].length == tl.length
566 566 lines[line_id - 1] << "</li>\n\t</#{ lT( depth[i] ) }l>\n\t"
567 567 depth.pop
568 568 end
569 569 end
570 570 if depth.last and depth.last.length == tl.length
571 571 lines[line_id - 1] << '</li>'
572 572 end
573 573 end
574 574 unless depth.last == tl
575 575 depth << tl
576 576 atts = pba( atts )
577 577 atts = shelve( atts ) if atts
578 578 lines[line_id] = "\t<#{ lT(tl) }l#{ atts }>\n\t<li>#{ content }"
579 579 else
580 580 lines[line_id] = "\t\t<li>#{ content }"
581 581 end
582 582 last_line = line_id
583 583
584 584 else
585 585 last_line = line_id
586 586 end
587 587 if line_id - last_line > 1 or line_id == lines.length - 1
588 588 while v = depth.pop
589 589 lines[last_line] << "</li>\n\t</#{ lT( v ) }l>"
590 590 end
591 591 end
592 592 end
593 593 lines.join( "\n" )
594 594 end
595 595 end
596 596
597 597 QUOTES_RE = /(^>+([^\n]*?)(\n|$))+/m
598 598 QUOTES_CONTENT_RE = /^([> ]+)(.*)$/m
599 599
600 600 def block_textile_quotes( text )
601 601 text.gsub!( QUOTES_RE ) do |match|
602 602 lines = match.split( /\n/ )
603 603 quotes = ''
604 604 indent = 0
605 605 lines.each do |line|
606 606 line =~ QUOTES_CONTENT_RE
607 607 bq,content = $1, $2
608 608 l = bq.count('>')
609 609 if l != indent
610 610 quotes << ("\n\n" + (l>indent ? '<blockquote>' * (l-indent) : '</blockquote>' * (indent-l)) + "\n\n")
611 611 indent = l
612 612 end
613 613 quotes << (content + "\n")
614 614 end
615 615 quotes << ("\n" + '</blockquote>' * indent + "\n\n")
616 616 quotes
617 617 end
618 618 end
619 619
620 620 CODE_RE = /(\W)
621 621 @
622 622 (?:\|(\w+?)\|)?
623 623 (.+?)
624 624 @
625 625 (?=\W)/x
626 626
627 627 def inline_textile_code( text )
628 628 text.gsub!( CODE_RE ) do |m|
629 629 before,lang,code,after = $~[1..4]
630 630 lang = " lang=\"#{ lang }\"" if lang
631 631 rip_offtags( "#{ before }<code#{ lang }>#{ code }</code>#{ after }", false )
632 632 end
633 633 end
634 634
635 635 def lT( text )
636 636 text =~ /\#$/ ? 'o' : 'u'
637 637 end
638 638
639 639 def hard_break( text )
640 640 text.gsub!( /(.)\n(?!\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
641 641 end
642 642
643 643 BLOCKS_GROUP_RE = /\n{2,}(?! )/m
644 644
645 645 def blocks( text, deep_code = false )
646 646 text.replace( text.split( BLOCKS_GROUP_RE ).collect do |blk|
647 647 plain = blk !~ /\A[#*> ]/
648 648
649 649 # skip blocks that are complex HTML
650 650 if blk =~ /^<\/?(\w+).*>/ and not SIMPLE_HTML_TAGS.include? $1
651 651 blk
652 652 else
653 653 # search for indentation levels
654 654 blk.strip!
655 655 if blk.empty?
656 656 blk
657 657 else
658 658 code_blk = nil
659 659 blk.gsub!( /((?:\n(?:\n^ +[^\n]*)+)+)/m ) do |iblk|
660 660 flush_left iblk
661 661 blocks iblk, plain
662 662 iblk.gsub( /^(\S)/, "\t\\1" )
663 663 if plain
664 664 code_blk = iblk; ""
665 665 else
666 666 iblk
667 667 end
668 668 end
669 669
670 670 block_applied = 0
671 671 @rules.each do |rule_name|
672 672 block_applied += 1 if ( rule_name.to_s.match /^block_/ and method( rule_name ).call( blk ) )
673 673 end
674 674 if block_applied.zero?
675 675 if deep_code
676 676 blk = "\t<pre><code>#{ blk }</code></pre>"
677 677 else
678 678 blk = "\t<p>#{ blk }</p>"
679 679 end
680 680 end
681 681 # hard_break blk
682 682 blk + "\n#{ code_blk }"
683 683 end
684 684 end
685 685
686 686 end.join( "\n\n" ) )
687 687 end
688 688
689 689 def textile_bq( tag, atts, cite, content )
690 690 cite, cite_title = check_refs( cite )
691 691 cite = " cite=\"#{ cite }\"" if cite
692 692 atts = shelve( atts ) if atts
693 693 "\t<blockquote#{ cite }>\n\t\t<p#{ atts }>#{ content }</p>\n\t</blockquote>"
694 694 end
695 695
696 696 def textile_p( tag, atts, cite, content )
697 697 atts = shelve( atts ) if atts
698 698 "\t<#{ tag }#{ atts }>#{ content }</#{ tag }>"
699 699 end
700 700
701 701 alias textile_h1 textile_p
702 702 alias textile_h2 textile_p
703 703 alias textile_h3 textile_p
704 704 alias textile_h4 textile_p
705 705 alias textile_h5 textile_p
706 706 alias textile_h6 textile_p
707 707
708 708 def textile_fn_( tag, num, atts, cite, content )
709 709 atts << " id=\"fn#{ num }\" class=\"footnote\""
710 710 content = "<sup>#{ num }</sup> #{ content }"
711 711 atts = shelve( atts ) if atts
712 712 "\t<p#{ atts }>#{ content }</p>"
713 713 end
714 714
715 715 BLOCK_RE = /^(([a-z]+)(\d*))(#{A}#{C})\.(?::(\S+))? (.*)$/m
716 716
717 717 def block_textile_prefix( text )
718 718 if text =~ BLOCK_RE
719 719 tag,tagpre,num,atts,cite,content = $~[1..6]
720 720 atts = pba( atts )
721 721
722 722 # pass to prefix handler
723 723 replacement = nil
724 724 if respond_to? "textile_#{ tag }", true
725 725 replacement = method( "textile_#{ tag }" ).call( tag, atts, cite, content )
726 726 elsif respond_to? "textile_#{ tagpre }_", true
727 727 replacement = method( "textile_#{ tagpre }_" ).call( tagpre, num, atts, cite, content )
728 728 end
729 729 text.gsub!( $& ) { replacement } if replacement
730 730 end
731 731 end
732 732
733 733 SETEXT_RE = /\A(.+?)\n([=-])[=-]* *$/m
734 734 def block_markdown_setext( text )
735 735 if text =~ SETEXT_RE
736 736 tag = if $2 == "="; "h1"; else; "h2"; end
737 737 blk, cont = "<#{ tag }>#{ $1 }</#{ tag }>", $'
738 738 blocks cont
739 739 text.replace( blk + cont )
740 740 end
741 741 end
742 742
743 743 ATX_RE = /\A(\#{1,6}) # $1 = string of #'s
744 744 [ ]*
745 745 (.+?) # $2 = Header text
746 746 [ ]*
747 747 \#* # optional closing #'s (not counted)
748 748 $/x
749 749 def block_markdown_atx( text )
750 750 if text =~ ATX_RE
751 751 tag = "h#{ $1.length }"
752 752 blk, cont = "<#{ tag }>#{ $2 }</#{ tag }>\n\n", $'
753 753 blocks cont
754 754 text.replace( blk + cont )
755 755 end
756 756 end
757 757
758 758 MARKDOWN_BQ_RE = /\A(^ *> ?.+$(.+\n)*\n*)+/m
759 759
760 760 def block_markdown_bq( text )
761 761 text.gsub!( MARKDOWN_BQ_RE ) do |blk|
762 762 blk.gsub!( /^ *> ?/, '' )
763 763 flush_left blk
764 764 blocks blk
765 765 blk.gsub!( /^(\S)/, "\t\\1" )
766 766 "<blockquote>\n#{ blk }\n</blockquote>\n\n"
767 767 end
768 768 end
769 769
770 770 MARKDOWN_RULE_RE = /^(#{
771 771 ['*', '-', '_'].collect { |ch| ' ?(' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' )
772 772 })$/
773 773
774 774 def block_markdown_rule( text )
775 775 text.gsub!( MARKDOWN_RULE_RE ) do |blk|
776 776 "<hr />"
777 777 end
778 778 end
779 779
780 780 # XXX TODO XXX
781 781 def block_markdown_lists( text )
782 782 end
783 783
784 784 def inline_textile_span( text )
785 785 QTAGS.each do |qtag_rc, ht, qtag_re, rtype|
786 786 text.gsub!( qtag_re ) do |m|
787 787
788 788 case rtype
789 789 when :limit
790 790 sta,oqs,qtag,content,oqa = $~[1..6]
791 791 atts = nil
792 792 if content =~ /^(#{C})(.+)$/
793 793 atts, content = $~[1..2]
794 794 end
795 795 else
796 796 qtag,atts,cite,content = $~[1..4]
797 797 sta = ''
798 798 end
799 799 atts = pba( atts )
800 800 atts = shelve( atts ) if atts
801 801
802 802 "#{ sta }#{ oqs }<#{ ht }#{ atts }>#{ content }</#{ ht }>#{ oqa }"
803 803
804 804 end
805 805 end
806 806 end
807 807
808 808 LINK_RE = /
809 809 (
810 810 ([\s\[{(]|[#{PUNCT}])? # $pre
811 811 " # start
812 812 (#{C}) # $atts
813 813 ([^"\n]+?) # $text
814 814 \s?
815 815 (?:\(([^)]+?)\)(?="))? # $title
816 816 ":
817 817 ( # $url
818 818 (\/|[a-zA-Z]+:\/\/|www\.|mailto:) # $proto
819 819 [[:alnum:]_\/]\S+?
820 820 )
821 821 (\/)? # $slash
822 822 ([^[:alnum:]_\=\/;\(\)]*?) # $post
823 823 )
824 824 (?=<|\s|$)
825 825 /x
826 826 #"
827 827 def inline_textile_link( text )
828 828 text.gsub!( LINK_RE ) do |m|
829 829 all,pre,atts,text,title,url,proto,slash,post = $~[1..9]
830 830 if text.include?('<br />')
831 831 all
832 832 else
833 833 url, url_title = check_refs( url )
834 834 title ||= url_title
835 835
836 836 # Idea below : an URL with unbalanced parethesis and
837 837 # ending by ')' is put into external parenthesis
838 838 if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
839 839 url=url[0..-2] # discard closing parenth from url
840 840 post = ")"+post # add closing parenth to post
841 841 end
842 842 atts = pba( atts )
843 843 atts = " href=\"#{ htmlesc url }#{ slash }\"#{ atts }"
844 844 atts << " title=\"#{ htmlesc title }\"" if title
845 845 atts = shelve( atts ) if atts
846 846
847 847 external = (url =~ /^https?:\/\//) ? ' class="external"' : ''
848 848
849 849 "#{ pre }<a#{ atts }#{ external }>#{ text }</a>#{ post }"
850 850 end
851 851 end
852 852 end
853 853
854 854 MARKDOWN_REFLINK_RE = /
855 855 \[([^\[\]]+)\] # $text
856 856 [ ]? # opt. space
857 857 (?:\n[ ]*)? # one optional newline followed by spaces
858 858 \[(.*?)\] # $id
859 859 /x
860 860
861 861 def inline_markdown_reflink( text )
862 862 text.gsub!( MARKDOWN_REFLINK_RE ) do |m|
863 863 text, id = $~[1..2]
864 864
865 865 if id.empty?
866 866 url, title = check_refs( text )
867 867 else
868 868 url, title = check_refs( id )
869 869 end
870 870
871 871 atts = " href=\"#{ url }\""
872 872 atts << " title=\"#{ title }\"" if title
873 873 atts = shelve( atts )
874 874
875 875 "<a#{ atts }>#{ text }</a>"
876 876 end
877 877 end
878 878
879 879 MARKDOWN_LINK_RE = /
880 880 \[([^\[\]]+)\] # $text
881 881 \( # open paren
882 882 [ \t]* # opt space
883 883 <?(.+?)>? # $href
884 884 [ \t]* # opt space
885 885 (?: # whole title
886 886 (['"]) # $quote
887 887 (.*?) # $title
888 888 \3 # matching quote
889 889 )? # title is optional
890 890 \)
891 891 /x
892 892
893 893 def inline_markdown_link( text )
894 894 text.gsub!( MARKDOWN_LINK_RE ) do |m|
895 895 text, url, quote, title = $~[1..4]
896 896
897 897 atts = " href=\"#{ url }\""
898 898 atts << " title=\"#{ title }\"" if title
899 899 atts = shelve( atts )
900 900
901 901 "<a#{ atts }>#{ text }</a>"
902 902 end
903 903 end
904 904
905 905 TEXTILE_REFS_RE = /(^ *)\[([^\[\n]+?)\](#{HYPERLINK})(?=\s|$)/
906 906 MARKDOWN_REFS_RE = /(^ *)\[([^\n]+?)\]:\s+<?(#{HYPERLINK})>?(?:\s+"((?:[^"]|\\")+)")?(?=\s|$)/m
907 907
908 908 def refs( text )
909 909 @rules.each do |rule_name|
910 910 method( rule_name ).call( text ) if rule_name.to_s.match /^refs_/
911 911 end
912 912 end
913 913
914 914 def refs_textile( text )
915 915 text.gsub!( TEXTILE_REFS_RE ) do |m|
916 916 flag, url = $~[2..3]
917 917 @urlrefs[flag.downcase] = [url, nil]
918 918 nil
919 919 end
920 920 end
921 921
922 922 def refs_markdown( text )
923 923 text.gsub!( MARKDOWN_REFS_RE ) do |m|
924 924 flag, url = $~[2..3]
925 925 title = $~[6]
926 926 @urlrefs[flag.downcase] = [url, title]
927 927 nil
928 928 end
929 929 end
930 930
931 931 def check_refs( text )
932 932 ret = @urlrefs[text.downcase] if text
933 933 ret || [text, nil]
934 934 end
935 935
936 936 IMAGE_RE = /
937 937 (>|\s|^) # start of line?
938 938 \! # opening
939 939 (\<|\=|\>)? # optional alignment atts
940 940 (#{C}) # optional style,class atts
941 941 (?:\. )? # optional dot-space
942 942 ([^\s(!]+?) # presume this is the src
943 943 \s? # optional space
944 944 (?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title
945 945 \! # closing
946 946 (?::#{ HYPERLINK })? # optional href
947 947 /x
948 948
949 949 def inline_textile_image( text )
950 950 text.gsub!( IMAGE_RE ) do |m|
951 951 stln,algn,atts,url,title,href,href_a1,href_a2 = $~[1..8]
952 952 htmlesc title
953 953 atts = pba( atts )
954 954 atts = " src=\"#{ htmlesc url.dup }\"#{ atts }"
955 955 atts << " title=\"#{ title }\"" if title
956 956 atts << " alt=\"#{ title }\""
957 957 # size = @getimagesize($url);
958 958 # if($size) $atts.= " $size[3]";
959 959
960 960 href, alt_title = check_refs( href ) if href
961 961 url, url_title = check_refs( url )
962 962
963 963 out = ''
964 964 out << "<a#{ shelve( " href=\"#{ href }\"" ) }>" if href
965 965 out << "<img#{ shelve( atts ) } />"
966 966 out << "</a>#{ href_a1 }#{ href_a2 }" if href
967 967
968 968 if algn
969 969 algn = h_align( algn )
970 970 if stln == "<p>"
971 971 out = "<p style=\"float:#{ algn }\">#{ out }"
972 972 else
973 973 out = "#{ stln }<div style=\"float:#{ algn }\">#{ out }</div>"
974 974 end
975 975 else
976 976 out = stln + out
977 977 end
978 978
979 979 out
980 980 end
981 981 end
982 982
983 983 def shelve( val )
984 984 @shelf << val
985 985 " :redsh##{ @shelf.length }:"
986 986 end
987 987
988 988 def retrieve( text )
989 989 text.gsub!(/ :redsh#(\d+):/) do
990 990 @shelf[$1.to_i - 1] || $&
991 991 end
992 992 end
993 993
994 994 def incoming_entities( text )
995 995 ## turn any incoming ampersands into a dummy character for now.
996 996 ## This uses a negative lookahead for alphanumerics followed by a semicolon,
997 997 ## implying an incoming html entity, to be skipped
998 998
999 999 text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" )
1000 1000 end
1001 1001
1002 1002 def no_textile( text )
1003 1003 text.gsub!( /(^|\s)==([^=]+.*?)==(\s|$)?/,
1004 1004 '\1<notextile>\2</notextile>\3' )
1005 1005 text.gsub!( /^ *==([^=]+.*?)==/m,
1006 1006 '\1<notextile>\2</notextile>\3' )
1007 1007 end
1008 1008
1009 1009 def clean_white_space( text )
1010 1010 # normalize line breaks
1011 1011 text.gsub!( /\r\n/, "\n" )
1012 1012 text.gsub!( /\r/, "\n" )
1013 1013 text.gsub!( /\t/, ' ' )
1014 1014 text.gsub!( /^ +$/, '' )
1015 1015 text.gsub!( /\n{3,}/, "\n\n" )
1016 1016 text.gsub!( /"$/, "\" " )
1017 1017
1018 1018 # if entire document is indented, flush
1019 1019 # to the left side
1020 1020 flush_left text
1021 1021 end
1022 1022
1023 1023 def flush_left( text )
1024 1024 indt = 0
1025 1025 if text =~ /^ /
1026 1026 while text !~ /^ {#{indt}}\S/
1027 1027 indt += 1
1028 1028 end unless text.empty?
1029 1029 if indt.nonzero?
1030 1030 text.gsub!( /^ {#{indt}}/, '' )
1031 1031 end
1032 1032 end
1033 1033 end
1034 1034
1035 1035 def footnote_ref( text )
1036 1036 text.gsub!( /\b\[([0-9]+?)\](\s)?/,
1037 1037 '<sup><a href="#fn\1">\1</a></sup>\2' )
1038 1038 end
1039 1039
1040 1040 OFFTAGS = /(code|pre|kbd|notextile)/
1041 1041 OFFTAG_MATCH = /(?:(<\/#{ OFFTAGS }>)|(<#{ OFFTAGS }[^>]*>))(.*?)(?=<\/?#{ OFFTAGS }\W|\Z)/mi
1042 1042 OFFTAG_OPEN = /<#{ OFFTAGS }/
1043 1043 OFFTAG_CLOSE = /<\/?#{ OFFTAGS }/
1044 1044 HASTAG_MATCH = /(<\/?\w[^\n]*?>)/m
1045 1045 ALLTAG_MATCH = /(<\/?\w[^\n]*?>)|.*?(?=<\/?\w[^\n]*?>|$)/m
1046 1046
1047 1047 def glyphs_textile( text, level = 0 )
1048 1048 if text !~ HASTAG_MATCH
1049 1049 pgl text
1050 1050 footnote_ref text
1051 1051 else
1052 1052 codepre = 0
1053 1053 text.gsub!( ALLTAG_MATCH ) do |line|
1054 1054 ## matches are off if we're between <code>, <pre> etc.
1055 1055 if $1
1056 1056 if line =~ OFFTAG_OPEN
1057 1057 codepre += 1
1058 1058 elsif line =~ OFFTAG_CLOSE
1059 1059 codepre -= 1
1060 1060 codepre = 0 if codepre < 0
1061 1061 end
1062 1062 elsif codepre.zero?
1063 1063 glyphs_textile( line, level + 1 )
1064 1064 else
1065 1065 htmlesc( line, :NoQuotes )
1066 1066 end
1067 1067 # p [level, codepre, line]
1068 1068
1069 1069 line
1070 1070 end
1071 1071 end
1072 1072 end
1073 1073
1074 1074 def rip_offtags( text, escape_aftertag=true, escape_line=true )
1075 1075 if text =~ /<.*>/
1076 1076 ## strip and encode <pre> content
1077 1077 codepre, used_offtags = 0, {}
1078 1078 text.gsub!( OFFTAG_MATCH ) do |line|
1079 1079 if $3
1080 1080 first, offtag, aftertag = $3, $4, $5
1081 1081 codepre += 1
1082 1082 used_offtags[offtag] = true
1083 1083 if codepre - used_offtags.length > 0
1084 1084 htmlesc( line, :NoQuotes ) if escape_line
1085 1085 @pre_list.last << line
1086 1086 line = ""
1087 1087 else
1088 1088 ### htmlesc is disabled between CODE tags which will be parsed with highlighter
1089 1089 ### Regexp in formatter.rb is : /<code\s+class="(\w+)">\s?(.+)/m
1090 1090 ### NB: some changes were made not to use $N variables, because we use "match"
1091 1091 ### and it breaks following lines
1092 1092 htmlesc( aftertag, :NoQuotes ) if aftertag && escape_aftertag && !first.match(/<code\s+class="(\w+)">/)
1093 1093 line = "<redpre##{ @pre_list.length }>"
1094 1094 first.match(/<#{ OFFTAGS }([^>]*)>/)
1095 1095 tag = $1
1096 1096 $2.to_s.match(/(class\=("[^"]+"|'[^']+'))/i)
1097 1097 tag << " #{$1}" if $1
1098 1098 @pre_list << "<#{ tag }>#{ aftertag }"
1099 1099 end
1100 1100 elsif $1 and codepre > 0
1101 1101 if codepre - used_offtags.length > 0
1102 1102 htmlesc( line, :NoQuotes ) if escape_line
1103 1103 @pre_list.last << line
1104 1104 line = ""
1105 1105 end
1106 1106 codepre -= 1 unless codepre.zero?
1107 1107 used_offtags = {} if codepre.zero?
1108 1108 end
1109 1109 line
1110 1110 end
1111 1111 end
1112 1112 text
1113 1113 end
1114 1114
1115 1115 def smooth_offtags( text )
1116 1116 unless @pre_list.empty?
1117 1117 ## replace <pre> content
1118 1118 text.gsub!( /<redpre#(\d+)>/ ) { @pre_list[$1.to_i] }
1119 1119 end
1120 1120 end
1121 1121
1122 1122 def inline( text )
1123 1123 [/^inline_/, /^glyphs_/].each do |meth_re|
1124 1124 @rules.each do |rule_name|
1125 1125 method( rule_name ).call( text ) if rule_name.to_s.match( meth_re )
1126 1126 end
1127 1127 end
1128 1128 end
1129 1129
1130 1130 def h_align( text )
1131 1131 H_ALGN_VALS[text]
1132 1132 end
1133 1133
1134 1134 def v_align( text )
1135 1135 V_ALGN_VALS[text]
1136 1136 end
1137 1137
1138 1138 def textile_popup_help( name, windowW, windowH )
1139 1139 ' <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 />'
1140 1140 end
1141 1141
1142 1142 # HTML cleansing stuff
1143 1143 BASIC_TAGS = {
1144 1144 'a' => ['href', 'title'],
1145 1145 'img' => ['src', 'alt', 'title'],
1146 1146 'br' => [],
1147 1147 'i' => nil,
1148 1148 'u' => nil,
1149 1149 'b' => nil,
1150 1150 'pre' => nil,
1151 1151 'kbd' => nil,
1152 1152 'code' => ['lang'],
1153 1153 'cite' => nil,
1154 1154 'strong' => nil,
1155 1155 'em' => nil,
1156 1156 'ins' => nil,
1157 1157 'sup' => nil,
1158 1158 'sub' => nil,
1159 1159 'del' => nil,
1160 1160 'table' => nil,
1161 1161 'tr' => nil,
1162 1162 'td' => ['colspan', 'rowspan'],
1163 1163 'th' => nil,
1164 1164 'ol' => nil,
1165 1165 'ul' => nil,
1166 1166 'li' => nil,
1167 1167 'p' => nil,
1168 1168 'h1' => nil,
1169 1169 'h2' => nil,
1170 1170 'h3' => nil,
1171 1171 'h4' => nil,
1172 1172 'h5' => nil,
1173 1173 'h6' => nil,
1174 1174 'blockquote' => ['cite']
1175 1175 }
1176 1176
1177 1177 def clean_html( text, tags = BASIC_TAGS )
1178 1178 text.gsub!( /<!\[CDATA\[/, '' )
1179 1179 text.gsub!( /<(\/*)(\w+)([^>]*)>/ ) do
1180 1180 raw = $~
1181 1181 tag = raw[2].downcase
1182 1182 if tags.has_key? tag
1183 1183 pcs = [tag]
1184 1184 tags[tag].each do |prop|
1185 1185 ['"', "'", ''].each do |q|
1186 1186 q2 = ( q != '' ? q : '\s' )
1187 1187 if raw[3] =~ /#{prop}\s*=\s*#{q}([^#{q2}]+)#{q}/i
1188 1188 attrv = $1
1189 1189 next if prop == 'src' and attrv =~ %r{^(?!http)\w+:}
1190 1190 pcs << "#{prop}=\"#{$1.gsub('"', '\\"')}\""
1191 1191 break
1192 1192 end
1193 1193 end
1194 1194 end if tags[tag]
1195 1195 "<#{raw[1]}#{pcs.join " "}>"
1196 1196 else
1197 1197 " "
1198 1198 end
1199 1199 end
1200 1200 end
1201 1201
1202 1202 ALLOWED_TAGS = %w(redpre pre code notextile)
1203 1203
1204 1204 def escape_html_tags(text)
1205 text.gsub!(%r{<(\/?([!\w]+)[^<>\n]*)?(>?)}) {|m| $2 && ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "&lt;#{$1}#{'&gt;' unless $3.blank?}" }
1205 text.gsub!(%r{<(\/?([!\w]+)[^<>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "&lt;#{$1}#{'&gt;' unless $3.blank?}" }
1206 1206 end
1207 1207 end
1208 1208
@@ -1,541 +1,534
1 1 #encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2015 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 require File.expand_path('../../../../../test_helper', __FILE__)
21 21 require 'digest/md5'
22 22
23 23 class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase
24 24
25 25 def setup
26 26 @formatter = Redmine::WikiFormatting::Textile::Formatter
27 27 end
28 28
29 29 MODIFIERS = {
30 30 "*" => 'strong', # bold
31 31 "_" => 'em', # italic
32 32 "+" => 'ins', # underline
33 33 "-" => 'del', # deleted
34 34 "^" => 'sup', # superscript
35 35 "~" => 'sub' # subscript
36 36 }
37 37
38 38 def test_modifiers
39 39 assert_html_output(
40 40 '*bold*' => '<strong>bold</strong>',
41 41 'before *bold*' => 'before <strong>bold</strong>',
42 42 '*bold* after' => '<strong>bold</strong> after',
43 43 '*two words*' => '<strong>two words</strong>',
44 44 '*two*words*' => '<strong>two*words</strong>',
45 45 '*two * words*' => '<strong>two * words</strong>',
46 46 '*two* *words*' => '<strong>two</strong> <strong>words</strong>',
47 47 '*(two)* *(words)*' => '<strong>(two)</strong> <strong>(words)</strong>',
48 48 # with class
49 49 '*(foo)two words*' => '<strong class="foo">two words</strong>'
50 50 )
51 51 end
52 52
53 53 def test_modifiers_combination
54 54 MODIFIERS.each do |m1, tag1|
55 55 MODIFIERS.each do |m2, tag2|
56 56 next if m1 == m2
57 57 text = "#{m2}#{m1}Phrase modifiers#{m1}#{m2}"
58 58 html = "<#{tag2}><#{tag1}>Phrase modifiers</#{tag1}></#{tag2}>"
59 59 assert_html_output text => html
60 60 end
61 61 end
62 62 end
63 63
64 64 def test_modifier_should_work_with_one_non_ascii_character
65 65 assert_html_output "*Γ„*" => "<strong>Γ„</strong>"
66 66 end
67 67
68 68 def test_styles
69 69 # single style
70 70 assert_html_output({
71 71 'p{color:red}. text' => '<p style="color:red;">text</p>',
72 72 'p{color:red;}. text' => '<p style="color:red;">text</p>',
73 73 'p{color: red}. text' => '<p style="color: red;">text</p>',
74 74 'p{color:#f00}. text' => '<p style="color:#f00;">text</p>',
75 75 'p{color:#ff0000}. text' => '<p style="color:#ff0000;">text</p>',
76 76 'p{border:10px}. text' => '<p style="border:10px;">text</p>',
77 77 'p{border:10}. text' => '<p style="border:10;">text</p>',
78 78 'p{border:10%}. text' => '<p style="border:10%;">text</p>',
79 79 'p{border:10em}. text' => '<p style="border:10em;">text</p>',
80 80 'p{border:1.5em}. text' => '<p style="border:1.5em;">text</p>',
81 81 'p{border-left:1px}. text' => '<p style="border-left:1px;">text</p>',
82 82 'p{border-right:1px}. text' => '<p style="border-right:1px;">text</p>',
83 83 'p{border-top:1px}. text' => '<p style="border-top:1px;">text</p>',
84 84 'p{border-bottom:1px}. text' => '<p style="border-bottom:1px;">text</p>',
85 85 }, false)
86 86
87 87 # multiple styles
88 88 assert_html_output({
89 89 'p{color:red; border-top:1px}. text' => '<p style="color:red;border-top:1px;">text</p>',
90 90 'p{color:red ; border-top:1px}. text' => '<p style="color:red;border-top:1px;">text</p>',
91 91 'p{color:red;border-top:1px}. text' => '<p style="color:red;border-top:1px;">text</p>',
92 92 }, false)
93 93
94 94 # styles with multiple values
95 95 assert_html_output({
96 96 'p{border:1px solid red;}. text' => '<p style="border:1px solid red;">text</p>',
97 97 'p{border-top-left-radius: 10px 5px;}. text' => '<p style="border-top-left-radius: 10px 5px;">text</p>',
98 98 }, false)
99 99 end
100 100
101 101 def test_invalid_styles_should_be_filtered
102 102 assert_html_output({
103 103 'p{invalid}. text' => '<p>text</p>',
104 104 'p{invalid:red}. text' => '<p>text</p>',
105 105 'p{color:(red)}. text' => '<p>text</p>',
106 106 'p{color:red;invalid:blue}. text' => '<p style="color:red;">text</p>',
107 107 'p{invalid:blue;color:red}. text' => '<p style="color:red;">text</p>',
108 108 'p{color:"}. text' => '<p>p{color:"}. text</p>',
109 109 }, false)
110 110 end
111 111
112 112 def test_inline_code
113 113 assert_html_output(
114 114 'this is @some code@' => 'this is <code>some code</code>',
115 115 '@<Location /redmine>@' => '<code>&lt;Location /redmine&gt;</code>'
116 116 )
117 117 end
118 118
119 119 def test_lang_attribute
120 120 assert_html_output(
121 121 '*[fr]French*' => '<strong lang="fr">French</strong>',
122 122 '*[fr-fr]French*' => '<strong lang="fr-fr">French</strong>',
123 123 '*[fr_fr]French*' => '<strong lang="fr_fr">French</strong>'
124 124 )
125 125 end
126 126
127 127 def test_lang_attribute_should_ignore_invalid_value
128 128 assert_html_output(
129 129 '*[fr3]French*' => '<strong>[fr3]French</strong>'
130 130 )
131 131 end
132 132
133 133 def test_nested_lists
134 134 raw = <<-RAW
135 135 # Item 1
136 136 # Item 2
137 137 ** Item 2a
138 138 ** Item 2b
139 139 # Item 3
140 140 ** Item 3a
141 141 RAW
142 142
143 143 expected = <<-EXPECTED
144 144 <ol>
145 145 <li>Item 1</li>
146 146 <li>Item 2
147 147 <ul>
148 148 <li>Item 2a</li>
149 149 <li>Item 2b</li>
150 150 </ul>
151 151 </li>
152 152 <li>Item 3
153 153 <ul>
154 154 <li>Item 3a</li>
155 155 </ul>
156 156 </li>
157 157 </ol>
158 158 EXPECTED
159 159
160 160 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
161 161 end
162 162
163 def test_should_escape_unallowed_tags
163 def test_escaping
164 164 assert_html_output(
165 165 'this is a <script>' => 'this is a &lt;script&gt;'
166 166 )
167 167 end
168 168
169 def test_should_escape_less_than_signs
170 assert_html_output(
171 '<' => '&lt;',
172 '1 < 2' => '1 &lt; 2'
173 )
174 end
175
176 169 def test_use_of_backslashes_followed_by_numbers_in_headers
177 170 assert_html_output({
178 171 'h1. 2009\02\09' => '<h1>2009\02\09</h1>'
179 172 }, false)
180 173 end
181 174
182 175 def test_double_dashes_should_not_strikethrough
183 176 assert_html_output(
184 177 'double -- dashes -- test' => 'double -- dashes -- test',
185 178 'double -- *dashes* -- test' => 'double -- <strong>dashes</strong> -- test'
186 179 )
187 180 end
188 181
189 182 def test_abbreviations
190 183 assert_html_output(
191 184 'this is an abbreviation: GPL(General Public License)' => 'this is an abbreviation: <abbr title="General Public License">GPL</abbr>',
192 185 '2 letters JP(Jean-Philippe) abbreviation' => '2 letters <abbr title="Jean-Philippe">JP</abbr> abbreviation',
193 186 'GPL(This is a double-quoted "title")' => '<abbr title="This is a double-quoted &quot;title&quot;">GPL</abbr>'
194 187 )
195 188 end
196 189
197 190 def test_blockquote
198 191 # orig raw text
199 192 raw = <<-RAW
200 193 John said:
201 194 > Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
202 195 > Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
203 196 > * Donec odio lorem,
204 197 > * sagittis ac,
205 198 > * malesuada in,
206 199 > * adipiscing eu, dolor.
207 200 >
208 201 > >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
209 202 > Proin a tellus. Nam vel neque.
210 203
211 204 He's right.
212 205 RAW
213 206
214 207 # expected html
215 208 expected = <<-EXPECTED
216 209 <p>John said:</p>
217 210 <blockquote>
218 211 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.<br />
219 212 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
220 213 <ul>
221 214 <li>Donec odio lorem,</li>
222 215 <li>sagittis ac,</li>
223 216 <li>malesuada in,</li>
224 217 <li>adipiscing eu, dolor.</li>
225 218 </ul>
226 219 <blockquote>
227 220 <p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
228 221 </blockquote>
229 222 <p>Proin a tellus. Nam vel neque.</p>
230 223 </blockquote>
231 224 <p>He's right.</p>
232 225 EXPECTED
233 226
234 227 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
235 228 end
236 229
237 230 def test_table
238 231 raw = <<-RAW
239 232 This is a table with empty cells:
240 233
241 234 |cell11|cell12||
242 235 |cell21||cell23|
243 236 |cell31|cell32|cell33|
244 237 RAW
245 238
246 239 expected = <<-EXPECTED
247 240 <p>This is a table with empty cells:</p>
248 241
249 242 <table>
250 243 <tr><td>cell11</td><td>cell12</td><td></td></tr>
251 244 <tr><td>cell21</td><td></td><td>cell23</td></tr>
252 245 <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
253 246 </table>
254 247 EXPECTED
255 248
256 249 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
257 250 end
258 251
259 252 def test_table_with_trailing_whitespace
260 253 raw = <<-RAW
261 254 This is a table with trailing whitespace in one row:
262 255
263 256 |cell11|cell12|
264 257 |cell21|cell22|
265 258 |cell31|cell32|
266 259 RAW
267 260
268 261 expected = <<-EXPECTED
269 262 <p>This is a table with trailing whitespace in one row:</p>
270 263
271 264 <table>
272 265 <tr><td>cell11</td><td>cell12</td></tr>
273 266 <tr><td>cell21</td><td>cell22</td></tr>
274 267 <tr><td>cell31</td><td>cell32</td></tr>
275 268 </table>
276 269 EXPECTED
277 270
278 271 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
279 272 end
280 273
281 274 def test_table_with_line_breaks
282 275 raw = <<-RAW
283 276 This is a table with line breaks:
284 277
285 278 |cell11
286 279 continued|cell12||
287 280 |-cell21-||cell23
288 281 cell23 line2
289 282 cell23 *line3*|
290 283 |cell31|cell32
291 284 cell32 line2|cell33|
292 285
293 286 RAW
294 287
295 288 expected = <<-EXPECTED
296 289 <p>This is a table with line breaks:</p>
297 290
298 291 <table>
299 292 <tr>
300 293 <td>cell11<br />continued</td>
301 294 <td>cell12</td>
302 295 <td></td>
303 296 </tr>
304 297 <tr>
305 298 <td><del>cell21</del></td>
306 299 <td></td>
307 300 <td>cell23<br/>cell23 line2<br/>cell23 <strong>line3</strong></td>
308 301 </tr>
309 302 <tr>
310 303 <td>cell31</td>
311 304 <td>cell32<br/>cell32 line2</td>
312 305 <td>cell33</td>
313 306 </tr>
314 307 </table>
315 308 EXPECTED
316 309
317 310 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
318 311 end
319 312
320 313 def test_tables_with_lists
321 314 raw = <<-RAW
322 315 This is a table with lists:
323 316
324 317 |cell11|cell12|
325 318 |cell21|ordered list
326 319 # item
327 320 # item 2|
328 321 |cell31|unordered list
329 322 * item
330 323 * item 2|
331 324
332 325 RAW
333 326
334 327 expected = <<-EXPECTED
335 328 <p>This is a table with lists:</p>
336 329
337 330 <table>
338 331 <tr>
339 332 <td>cell11</td>
340 333 <td>cell12</td>
341 334 </tr>
342 335 <tr>
343 336 <td>cell21</td>
344 337 <td>ordered list<br /># item<br /># item 2</td>
345 338 </tr>
346 339 <tr>
347 340 <td>cell31</td>
348 341 <td>unordered list<br />* item<br />* item 2</td>
349 342 </tr>
350 343 </table>
351 344 EXPECTED
352 345
353 346 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
354 347 end
355 348
356 349 def test_textile_should_not_mangle_brackets
357 350 assert_equal '<p>[msg1][msg2]</p>', to_html('[msg1][msg2]')
358 351 end
359 352
360 353 def test_textile_should_escape_image_urls
361 354 # this is onclick="alert('XSS');" in encoded form
362 355 raw = '!/images/comment.png"onclick=&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;&#x27;&#x58;&#x53;&#x53;&#x27;&#x29;;&#x22;!'
363 356 expected = '<p><img src="/images/comment.png&quot;onclick=&amp;#x61;&amp;#x6c;&amp;#x65;&amp;#x72;&amp;#x74;&amp;#x28;&amp;#x27;&amp;#x58;&amp;#x53;&amp;#x53;&amp;#x27;&amp;#x29;;&amp;#x22;" alt="" /></p>'
364 357 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
365 358 end
366 359
367 360
368 361 STR_WITHOUT_PRE = [
369 362 # 0
370 363 "h1. Title
371 364
372 365 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.",
373 366 # 1
374 367 "h2. Heading 2
375 368
376 369 Maecenas sed elit sit amet mi accumsan vestibulum non nec velit. Proin porta tincidunt lorem, consequat rhoncus dolor fermentum in.
377 370
378 371 Cras ipsum felis, ultrices at porttitor vel, faucibus eu nunc.",
379 372 # 2
380 373 "h2. Heading 2
381 374
382 375 Morbi facilisis accumsan orci non pharetra.
383 376
384 377 h3. Heading 3
385 378
386 379 Nulla nunc nisi, egestas in ornare vel, posuere ac libero.",
387 380 # 3
388 381 "h3. Heading 3
389 382
390 383 Praesent eget turpis nibh, a lacinia nulla.",
391 384 # 4
392 385 "h2. Heading 2
393 386
394 387 Ut rhoncus elementum adipiscing."]
395 388
396 389 TEXT_WITHOUT_PRE = STR_WITHOUT_PRE.join("\n\n").freeze
397 390
398 391 def test_get_section_should_return_the_requested_section_and_its_hash
399 392 assert_section_with_hash STR_WITHOUT_PRE[1], TEXT_WITHOUT_PRE, 2
400 393 assert_section_with_hash STR_WITHOUT_PRE[2..3].join("\n\n"), TEXT_WITHOUT_PRE, 3
401 394 assert_section_with_hash STR_WITHOUT_PRE[3], TEXT_WITHOUT_PRE, 5
402 395 assert_section_with_hash STR_WITHOUT_PRE[4], TEXT_WITHOUT_PRE, 6
403 396
404 397 assert_section_with_hash '', TEXT_WITHOUT_PRE, 0
405 398 assert_section_with_hash '', TEXT_WITHOUT_PRE, 10
406 399 end
407 400
408 401 def test_update_section_should_update_the_requested_section
409 402 replacement = "New text"
410 403
411 404 assert_equal [STR_WITHOUT_PRE[0], replacement, STR_WITHOUT_PRE[2..4]].flatten.join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(2, replacement)
412 405 assert_equal [STR_WITHOUT_PRE[0..1], replacement, STR_WITHOUT_PRE[4]].flatten.join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(3, replacement)
413 406 assert_equal [STR_WITHOUT_PRE[0..2], replacement, STR_WITHOUT_PRE[4]].flatten.join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(5, replacement)
414 407 assert_equal [STR_WITHOUT_PRE[0..3], replacement].flatten.join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(6, replacement)
415 408
416 409 assert_equal TEXT_WITHOUT_PRE, @formatter.new(TEXT_WITHOUT_PRE).update_section(0, replacement)
417 410 assert_equal TEXT_WITHOUT_PRE, @formatter.new(TEXT_WITHOUT_PRE).update_section(10, replacement)
418 411 end
419 412
420 413 def test_update_section_with_hash_should_update_the_requested_section
421 414 replacement = "New text"
422 415
423 416 assert_equal [STR_WITHOUT_PRE[0], replacement, STR_WITHOUT_PRE[2..4]].flatten.join("\n\n"),
424 417 @formatter.new(TEXT_WITHOUT_PRE).update_section(2, replacement, Digest::MD5.hexdigest(STR_WITHOUT_PRE[1]))
425 418 end
426 419
427 420 def test_update_section_with_wrong_hash_should_raise_an_error
428 421 assert_raise Redmine::WikiFormatting::StaleSectionError do
429 422 @formatter.new(TEXT_WITHOUT_PRE).update_section(2, "New text", Digest::MD5.hexdigest("Old text"))
430 423 end
431 424 end
432 425
433 426 STR_WITH_PRE = [
434 427 # 0
435 428 "h1. Title
436 429
437 430 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.",
438 431 # 1
439 432 "h2. Heading 2
440 433
441 434 <pre><code class=\"ruby\">
442 435 def foo
443 436 end
444 437 </code></pre>
445 438
446 439 <pre><code><pre><code class=\"ruby\">
447 440 Place your code here.
448 441 </code></pre>
449 442 </code></pre>
450 443
451 444 Morbi facilisis accumsan orci non pharetra.
452 445
453 446 <pre>
454 447 Pre Content:
455 448
456 449 h2. Inside pre
457 450
458 451 <tag> inside pre block
459 452
460 453 Morbi facilisis accumsan orci non pharetra.
461 454 </pre>",
462 455 # 2
463 456 "h3. Heading 3
464 457
465 458 Nulla nunc nisi, egestas in ornare vel, posuere ac libero."]
466 459
467 460 def test_get_section_should_ignore_pre_content
468 461 text = STR_WITH_PRE.join("\n\n")
469 462
470 463 assert_section_with_hash STR_WITH_PRE[1..2].join("\n\n"), text, 2
471 464 assert_section_with_hash STR_WITH_PRE[2], text, 3
472 465 end
473 466
474 467 def test_update_section_should_not_escape_pre_content_outside_section
475 468 text = STR_WITH_PRE.join("\n\n")
476 469 replacement = "New text"
477 470
478 471 assert_equal [STR_WITH_PRE[0..1], "New text"].flatten.join("\n\n"),
479 472 @formatter.new(text).update_section(3, replacement)
480 473 end
481 474
482 475 def test_get_section_should_support_lines_with_spaces_before_heading
483 476 # the lines after Content 2 and Heading 4 contain a space
484 477 text = <<-STR
485 478 h1. Heading 1
486 479
487 480 Content 1
488 481
489 482 h1. Heading 2
490 483
491 484 Content 2
492 485
493 486 h1. Heading 3
494 487
495 488 Content 3
496 489
497 490 h1. Heading 4
498 491
499 492 Content 4
500 493 STR
501 494
502 495 [1, 2, 3, 4].each do |index|
503 496 assert_match /\Ah1. Heading #{index}.+Content #{index}/m, @formatter.new(text).get_section(index).first
504 497 end
505 498 end
506 499
507 500 def test_get_section_should_support_headings_starting_with_a_tab
508 501 text = <<-STR
509 502 h1.\tHeading 1
510 503
511 504 Content 1
512 505
513 506 h1. Heading 2
514 507
515 508 Content 2
516 509 STR
517 510
518 511 assert_match /\Ah1.\tHeading 1\s+Content 1\z/, @formatter.new(text).get_section(1).first
519 512 end
520 513
521 514 private
522 515
523 516 def assert_html_output(to_test, expect_paragraph = true)
524 517 to_test.each do |text, expected|
525 518 assert_equal(( expect_paragraph ? "<p>#{expected}</p>" : expected ), @formatter.new(text).to_html, "Formatting the following text failed:\n===\n#{text}\n===\n")
526 519 end
527 520 end
528 521
529 522 def to_html(text)
530 523 @formatter.new(text).to_html
531 524 end
532 525
533 526 def assert_section_with_hash(expected, text, index)
534 527 result = @formatter.new(text).get_section(index)
535 528
536 529 assert_kind_of Array, result
537 530 assert_equal 2, result.size
538 531 assert_equal expected, result.first, "section content did not match"
539 532 assert_equal Digest::MD5.hexdigest(expected), result.last, "section hash did not match"
540 533 end
541 534 end
General Comments 0
You need to be logged in to leave comments. Login now