##// END OF EJS Templates
Fixed that less-than sign is not escaped by textile formatter (#6969)....
Jean-Philippe Lang -
r14430:c6283d7ce59d
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| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "&lt;#{$1}#{'&gt;' unless $3.blank?}" }
1205 text.gsub!(%r{<(\/?([!\w]+)[^<>\n]*)?(>?)}) {|m| $2 && ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "&lt;#{$1}#{'&gt;' unless $3.blank?}" }
1206 1206 end
1207 1207 end
1208 1208
@@ -1,534 +1,541
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_escaping
163 def test_should_escape_unallowed_tags
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
169 176 def test_use_of_backslashes_followed_by_numbers_in_headers
170 177 assert_html_output({
171 178 'h1. 2009\02\09' => '<h1>2009\02\09</h1>'
172 179 }, false)
173 180 end
174 181
175 182 def test_double_dashes_should_not_strikethrough
176 183 assert_html_output(
177 184 'double -- dashes -- test' => 'double -- dashes -- test',
178 185 'double -- *dashes* -- test' => 'double -- <strong>dashes</strong> -- test'
179 186 )
180 187 end
181 188
182 189 def test_abbreviations
183 190 assert_html_output(
184 191 'this is an abbreviation: GPL(General Public License)' => 'this is an abbreviation: <abbr title="General Public License">GPL</abbr>',
185 192 '2 letters JP(Jean-Philippe) abbreviation' => '2 letters <abbr title="Jean-Philippe">JP</abbr> abbreviation',
186 193 'GPL(This is a double-quoted "title")' => '<abbr title="This is a double-quoted &quot;title&quot;">GPL</abbr>'
187 194 )
188 195 end
189 196
190 197 def test_blockquote
191 198 # orig raw text
192 199 raw = <<-RAW
193 200 John said:
194 201 > Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
195 202 > Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
196 203 > * Donec odio lorem,
197 204 > * sagittis ac,
198 205 > * malesuada in,
199 206 > * adipiscing eu, dolor.
200 207 >
201 208 > >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
202 209 > Proin a tellus. Nam vel neque.
203 210
204 211 He's right.
205 212 RAW
206 213
207 214 # expected html
208 215 expected = <<-EXPECTED
209 216 <p>John said:</p>
210 217 <blockquote>
211 218 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.<br />
212 219 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
213 220 <ul>
214 221 <li>Donec odio lorem,</li>
215 222 <li>sagittis ac,</li>
216 223 <li>malesuada in,</li>
217 224 <li>adipiscing eu, dolor.</li>
218 225 </ul>
219 226 <blockquote>
220 227 <p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
221 228 </blockquote>
222 229 <p>Proin a tellus. Nam vel neque.</p>
223 230 </blockquote>
224 231 <p>He's right.</p>
225 232 EXPECTED
226 233
227 234 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
228 235 end
229 236
230 237 def test_table
231 238 raw = <<-RAW
232 239 This is a table with empty cells:
233 240
234 241 |cell11|cell12||
235 242 |cell21||cell23|
236 243 |cell31|cell32|cell33|
237 244 RAW
238 245
239 246 expected = <<-EXPECTED
240 247 <p>This is a table with empty cells:</p>
241 248
242 249 <table>
243 250 <tr><td>cell11</td><td>cell12</td><td></td></tr>
244 251 <tr><td>cell21</td><td></td><td>cell23</td></tr>
245 252 <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
246 253 </table>
247 254 EXPECTED
248 255
249 256 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
250 257 end
251 258
252 259 def test_table_with_trailing_whitespace
253 260 raw = <<-RAW
254 261 This is a table with trailing whitespace in one row:
255 262
256 263 |cell11|cell12|
257 264 |cell21|cell22|
258 265 |cell31|cell32|
259 266 RAW
260 267
261 268 expected = <<-EXPECTED
262 269 <p>This is a table with trailing whitespace in one row:</p>
263 270
264 271 <table>
265 272 <tr><td>cell11</td><td>cell12</td></tr>
266 273 <tr><td>cell21</td><td>cell22</td></tr>
267 274 <tr><td>cell31</td><td>cell32</td></tr>
268 275 </table>
269 276 EXPECTED
270 277
271 278 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
272 279 end
273 280
274 281 def test_table_with_line_breaks
275 282 raw = <<-RAW
276 283 This is a table with line breaks:
277 284
278 285 |cell11
279 286 continued|cell12||
280 287 |-cell21-||cell23
281 288 cell23 line2
282 289 cell23 *line3*|
283 290 |cell31|cell32
284 291 cell32 line2|cell33|
285 292
286 293 RAW
287 294
288 295 expected = <<-EXPECTED
289 296 <p>This is a table with line breaks:</p>
290 297
291 298 <table>
292 299 <tr>
293 300 <td>cell11<br />continued</td>
294 301 <td>cell12</td>
295 302 <td></td>
296 303 </tr>
297 304 <tr>
298 305 <td><del>cell21</del></td>
299 306 <td></td>
300 307 <td>cell23<br/>cell23 line2<br/>cell23 <strong>line3</strong></td>
301 308 </tr>
302 309 <tr>
303 310 <td>cell31</td>
304 311 <td>cell32<br/>cell32 line2</td>
305 312 <td>cell33</td>
306 313 </tr>
307 314 </table>
308 315 EXPECTED
309 316
310 317 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
311 318 end
312 319
313 320 def test_tables_with_lists
314 321 raw = <<-RAW
315 322 This is a table with lists:
316 323
317 324 |cell11|cell12|
318 325 |cell21|ordered list
319 326 # item
320 327 # item 2|
321 328 |cell31|unordered list
322 329 * item
323 330 * item 2|
324 331
325 332 RAW
326 333
327 334 expected = <<-EXPECTED
328 335 <p>This is a table with lists:</p>
329 336
330 337 <table>
331 338 <tr>
332 339 <td>cell11</td>
333 340 <td>cell12</td>
334 341 </tr>
335 342 <tr>
336 343 <td>cell21</td>
337 344 <td>ordered list<br /># item<br /># item 2</td>
338 345 </tr>
339 346 <tr>
340 347 <td>cell31</td>
341 348 <td>unordered list<br />* item<br />* item 2</td>
342 349 </tr>
343 350 </table>
344 351 EXPECTED
345 352
346 353 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
347 354 end
348 355
349 356 def test_textile_should_not_mangle_brackets
350 357 assert_equal '<p>[msg1][msg2]</p>', to_html('[msg1][msg2]')
351 358 end
352 359
353 360 def test_textile_should_escape_image_urls
354 361 # this is onclick="alert('XSS');" in encoded form
355 362 raw = '!/images/comment.png"onclick=&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;&#x27;&#x58;&#x53;&#x53;&#x27;&#x29;;&#x22;!'
356 363 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>'
357 364 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
358 365 end
359 366
360 367
361 368 STR_WITHOUT_PRE = [
362 369 # 0
363 370 "h1. Title
364 371
365 372 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.",
366 373 # 1
367 374 "h2. Heading 2
368 375
369 376 Maecenas sed elit sit amet mi accumsan vestibulum non nec velit. Proin porta tincidunt lorem, consequat rhoncus dolor fermentum in.
370 377
371 378 Cras ipsum felis, ultrices at porttitor vel, faucibus eu nunc.",
372 379 # 2
373 380 "h2. Heading 2
374 381
375 382 Morbi facilisis accumsan orci non pharetra.
376 383
377 384 h3. Heading 3
378 385
379 386 Nulla nunc nisi, egestas in ornare vel, posuere ac libero.",
380 387 # 3
381 388 "h3. Heading 3
382 389
383 390 Praesent eget turpis nibh, a lacinia nulla.",
384 391 # 4
385 392 "h2. Heading 2
386 393
387 394 Ut rhoncus elementum adipiscing."]
388 395
389 396 TEXT_WITHOUT_PRE = STR_WITHOUT_PRE.join("\n\n").freeze
390 397
391 398 def test_get_section_should_return_the_requested_section_and_its_hash
392 399 assert_section_with_hash STR_WITHOUT_PRE[1], TEXT_WITHOUT_PRE, 2
393 400 assert_section_with_hash STR_WITHOUT_PRE[2..3].join("\n\n"), TEXT_WITHOUT_PRE, 3
394 401 assert_section_with_hash STR_WITHOUT_PRE[3], TEXT_WITHOUT_PRE, 5
395 402 assert_section_with_hash STR_WITHOUT_PRE[4], TEXT_WITHOUT_PRE, 6
396 403
397 404 assert_section_with_hash '', TEXT_WITHOUT_PRE, 0
398 405 assert_section_with_hash '', TEXT_WITHOUT_PRE, 10
399 406 end
400 407
401 408 def test_update_section_should_update_the_requested_section
402 409 replacement = "New text"
403 410
404 411 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)
405 412 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)
406 413 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)
407 414 assert_equal [STR_WITHOUT_PRE[0..3], replacement].flatten.join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(6, replacement)
408 415
409 416 assert_equal TEXT_WITHOUT_PRE, @formatter.new(TEXT_WITHOUT_PRE).update_section(0, replacement)
410 417 assert_equal TEXT_WITHOUT_PRE, @formatter.new(TEXT_WITHOUT_PRE).update_section(10, replacement)
411 418 end
412 419
413 420 def test_update_section_with_hash_should_update_the_requested_section
414 421 replacement = "New text"
415 422
416 423 assert_equal [STR_WITHOUT_PRE[0], replacement, STR_WITHOUT_PRE[2..4]].flatten.join("\n\n"),
417 424 @formatter.new(TEXT_WITHOUT_PRE).update_section(2, replacement, Digest::MD5.hexdigest(STR_WITHOUT_PRE[1]))
418 425 end
419 426
420 427 def test_update_section_with_wrong_hash_should_raise_an_error
421 428 assert_raise Redmine::WikiFormatting::StaleSectionError do
422 429 @formatter.new(TEXT_WITHOUT_PRE).update_section(2, "New text", Digest::MD5.hexdigest("Old text"))
423 430 end
424 431 end
425 432
426 433 STR_WITH_PRE = [
427 434 # 0
428 435 "h1. Title
429 436
430 437 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.",
431 438 # 1
432 439 "h2. Heading 2
433 440
434 441 <pre><code class=\"ruby\">
435 442 def foo
436 443 end
437 444 </code></pre>
438 445
439 446 <pre><code><pre><code class=\"ruby\">
440 447 Place your code here.
441 448 </code></pre>
442 449 </code></pre>
443 450
444 451 Morbi facilisis accumsan orci non pharetra.
445 452
446 453 <pre>
447 454 Pre Content:
448 455
449 456 h2. Inside pre
450 457
451 458 <tag> inside pre block
452 459
453 460 Morbi facilisis accumsan orci non pharetra.
454 461 </pre>",
455 462 # 2
456 463 "h3. Heading 3
457 464
458 465 Nulla nunc nisi, egestas in ornare vel, posuere ac libero."]
459 466
460 467 def test_get_section_should_ignore_pre_content
461 468 text = STR_WITH_PRE.join("\n\n")
462 469
463 470 assert_section_with_hash STR_WITH_PRE[1..2].join("\n\n"), text, 2
464 471 assert_section_with_hash STR_WITH_PRE[2], text, 3
465 472 end
466 473
467 474 def test_update_section_should_not_escape_pre_content_outside_section
468 475 text = STR_WITH_PRE.join("\n\n")
469 476 replacement = "New text"
470 477
471 478 assert_equal [STR_WITH_PRE[0..1], "New text"].flatten.join("\n\n"),
472 479 @formatter.new(text).update_section(3, replacement)
473 480 end
474 481
475 482 def test_get_section_should_support_lines_with_spaces_before_heading
476 483 # the lines after Content 2 and Heading 4 contain a space
477 484 text = <<-STR
478 485 h1. Heading 1
479 486
480 487 Content 1
481 488
482 489 h1. Heading 2
483 490
484 491 Content 2
485 492
486 493 h1. Heading 3
487 494
488 495 Content 3
489 496
490 497 h1. Heading 4
491 498
492 499 Content 4
493 500 STR
494 501
495 502 [1, 2, 3, 4].each do |index|
496 503 assert_match /\Ah1. Heading #{index}.+Content #{index}/m, @formatter.new(text).get_section(index).first
497 504 end
498 505 end
499 506
500 507 def test_get_section_should_support_headings_starting_with_a_tab
501 508 text = <<-STR
502 509 h1.\tHeading 1
503 510
504 511 Content 1
505 512
506 513 h1. Heading 2
507 514
508 515 Content 2
509 516 STR
510 517
511 518 assert_match /\Ah1.\tHeading 1\s+Content 1\z/, @formatter.new(text).get_section(1).first
512 519 end
513 520
514 521 private
515 522
516 523 def assert_html_output(to_test, expect_paragraph = true)
517 524 to_test.each do |text, expected|
518 525 assert_equal(( expect_paragraph ? "<p>#{expected}</p>" : expected ), @formatter.new(text).to_html, "Formatting the following text failed:\n===\n#{text}\n===\n")
519 526 end
520 527 end
521 528
522 529 def to_html(text)
523 530 @formatter.new(text).to_html
524 531 end
525 532
526 533 def assert_section_with_hash(expected, text, index)
527 534 result = @formatter.new(text).get_section(index)
528 535
529 536 assert_kind_of Array, result
530 537 assert_equal 2, result.size
531 538 assert_equal expected, result.first, "section content did not match"
532 539 assert_equal Digest::MD5.hexdigest(expected), result.last, "section hash did not match"
533 540 end
534 541 end
General Comments 0
You need to be logged in to leave comments. Login now