##// END OF EJS Templates
Do not recognize invalid textile attributes....
Jean-Philippe Lang -
r8782:37575f27feb1
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 # <acronym title="American Civil Liberties Union">ACLU</acronym>
133 133 #
134 134 # == Adding Tables
135 135 #
136 136 # In Textile, simple tables can be added by seperating each column by
137 137 # a pipe.
138 138 #
139 139 # |a|simple|table|row|
140 140 # |And|Another|table|row|
141 141 #
142 142 # Attributes are defined by style definitions in parentheses.
143 143 #
144 144 # table(border:1px solid black).
145 145 # (background:#ddd;color:red). |{}| | | |
146 146 #
147 147 # == Using RedCloth
148 148 #
149 149 # RedCloth is simply an extension of the String class, which can handle
150 150 # Textile formatting. Use it like a String and output HTML with its
151 151 # RedCloth#to_html method.
152 152 #
153 153 # doc = RedCloth.new "
154 154 #
155 155 # h2. Test document
156 156 #
157 157 # Just a simple test."
158 158 #
159 159 # puts doc.to_html
160 160 #
161 161 # By default, RedCloth uses both Textile and Markdown formatting, with
162 162 # Textile formatting taking precedence. If you want to turn off Markdown
163 163 # formatting, to boost speed and limit the processor:
164 164 #
165 165 # class RedCloth::Textile.new( str )
166 166
167 167 class RedCloth3 < String
168 168
169 169 VERSION = '3.0.4'
170 170 DEFAULT_RULES = [:textile, :markdown]
171 171
172 172 #
173 173 # Two accessor for setting security restrictions.
174 174 #
175 175 # This is a nice thing if you're using RedCloth for
176 176 # formatting in public places (e.g. Wikis) where you
177 177 # don't want users to abuse HTML for bad things.
178 178 #
179 179 # If +:filter_html+ is set, HTML which wasn't
180 180 # created by the Textile processor will be escaped.
181 181 #
182 182 # If +:filter_styles+ is set, it will also disable
183 183 # the style markup specifier. ('{color: red}')
184 184 #
185 185 attr_accessor :filter_html, :filter_styles
186 186
187 187 #
188 188 # Accessor for toggling hard breaks.
189 189 #
190 190 # If +:hard_breaks+ is set, single newlines will
191 191 # be converted to HTML break tags. This is the
192 192 # default behavior for traditional RedCloth.
193 193 #
194 194 attr_accessor :hard_breaks
195 195
196 196 # Accessor for toggling lite mode.
197 197 #
198 198 # In lite mode, block-level rules are ignored. This means
199 199 # that tables, paragraphs, lists, and such aren't available.
200 200 # Only the inline markup for bold, italics, entities and so on.
201 201 #
202 202 # r = RedCloth.new( "And then? She *fell*!", [:lite_mode] )
203 203 # r.to_html
204 204 # #=> "And then? She <strong>fell</strong>!"
205 205 #
206 206 attr_accessor :lite_mode
207 207
208 208 #
209 209 # Accessor for toggling span caps.
210 210 #
211 211 # Textile places `span' tags around capitalized
212 212 # words by default, but this wreaks havoc on Wikis.
213 213 # If +:no_span_caps+ is set, this will be
214 214 # suppressed.
215 215 #
216 216 attr_accessor :no_span_caps
217 217
218 218 #
219 219 # Establishes the markup predence. Available rules include:
220 220 #
221 221 # == Textile Rules
222 222 #
223 223 # The following textile rules can be set individually. Or add the complete
224 224 # set of rules with the single :textile rule, which supplies the rule set in
225 225 # the following precedence:
226 226 #
227 227 # refs_textile:: Textile references (i.e. [hobix]http://hobix.com/)
228 228 # block_textile_table:: Textile table block structures
229 229 # block_textile_lists:: Textile list structures
230 230 # block_textile_prefix:: Textile blocks with prefixes (i.e. bq., h2., etc.)
231 231 # inline_textile_image:: Textile inline images
232 232 # inline_textile_link:: Textile inline links
233 233 # inline_textile_span:: Textile inline spans
234 234 # glyphs_textile:: Textile entities (such as em-dashes and smart quotes)
235 235 #
236 236 # == Markdown
237 237 #
238 238 # refs_markdown:: Markdown references (for example: [hobix]: http://hobix.com/)
239 239 # block_markdown_setext:: Markdown setext headers
240 240 # block_markdown_atx:: Markdown atx headers
241 241 # block_markdown_rule:: Markdown horizontal rules
242 242 # block_markdown_bq:: Markdown blockquotes
243 243 # block_markdown_lists:: Markdown lists
244 244 # inline_markdown_link:: Markdown links
245 245 attr_accessor :rules
246 246
247 247 # Returns a new RedCloth object, based on _string_ and
248 248 # enforcing all the included _restrictions_.
249 249 #
250 250 # r = RedCloth.new( "h1. A <b>bold</b> man", [:filter_html] )
251 251 # r.to_html
252 252 # #=>"<h1>A &lt;b&gt;bold&lt;/b&gt; man</h1>"
253 253 #
254 254 def initialize( string, restrictions = [] )
255 255 restrictions.each { |r| method( "#{ r }=" ).call( true ) }
256 256 super( string )
257 257 end
258 258
259 259 #
260 260 # Generates HTML from the Textile contents.
261 261 #
262 262 # r = RedCloth.new( "And then? She *fell*!" )
263 263 # r.to_html( true )
264 264 # #=>"And then? She <strong>fell</strong>!"
265 265 #
266 266 def to_html( *rules )
267 267 rules = DEFAULT_RULES if rules.empty?
268 268 # make our working copy
269 269 text = self.dup
270 270
271 271 @urlrefs = {}
272 272 @shelf = []
273 273 textile_rules = [: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 C_CLAS = '(?:\([^)]+\))'
344 C_LNGE = '(?:\[[^\[\]]+\])'
345 C_STYL = '(?:\{[^}]+\})'
343 C_CLAS = '(?:\([^")]+\))'
344 C_LNGE = '(?:\[[^"\[\]]+\])'
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 (\w|[^\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 (\w|[^\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 "<acronym title=\"#{htmlesc $2}\">#{$1}</acronym>"
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!( /\[([^)]+?)\]/, '' )
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)(-[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
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 row.split( /(\|)(?![^\[\|]*\]\])/ )[1..-2].each do |cell|
533 533 next if cell == '|'
534 534 ctyp = 'd'
535 535 ctyp = 'h' if cell =~ /^_/
536 536
537 537 catts = ''
538 538 catts, cell = pba( $1, 'td' ), $2 if cell =~ /^(_?#{S}#{A}#{C}\. ?)(.*)/
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 depth.delete_if do |v|
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 [\w\/]\S+?
820 820 )
821 821 (\/)? # $slash
822 822 ([^\w\=\/;\(\)]*?) # $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 @shelf.each_with_index do |r, i|
990 990 text.gsub!( " :redsh##{ i + 1 }:", r )
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 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,387 +1,387
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../../../../test_helper', __FILE__)
19 19 require 'digest/md5'
20 20
21 21 class Redmine::WikiFormatting::TextileFormatterTest < ActionView::TestCase
22 22
23 23 def setup
24 24 @formatter = Redmine::WikiFormatting::Textile::Formatter
25 25 end
26 26
27 27 MODIFIERS = {
28 28 "*" => 'strong', # bold
29 29 "_" => 'em', # italic
30 30 "+" => 'ins', # underline
31 31 "-" => 'del', # deleted
32 32 "^" => 'sup', # superscript
33 33 "~" => 'sub' # subscript
34 34 }
35 35
36 36 def test_modifiers
37 37 assert_html_output(
38 38 '*bold*' => '<strong>bold</strong>',
39 39 'before *bold*' => 'before <strong>bold</strong>',
40 40 '*bold* after' => '<strong>bold</strong> after',
41 41 '*two words*' => '<strong>two words</strong>',
42 42 '*two*words*' => '<strong>two*words</strong>',
43 43 '*two * words*' => '<strong>two * words</strong>',
44 44 '*two* *words*' => '<strong>two</strong> <strong>words</strong>',
45 45 '*(two)* *(words)*' => '<strong>(two)</strong> <strong>(words)</strong>',
46 46 # with class
47 47 '*(foo)two words*' => '<strong class="foo">two words</strong>'
48 48 )
49 49 end
50 50
51 51 def test_modifiers_combination
52 52 MODIFIERS.each do |m1, tag1|
53 53 MODIFIERS.each do |m2, tag2|
54 54 next if m1 == m2
55 55 text = "#{m2}#{m1}Phrase modifiers#{m1}#{m2}"
56 56 html = "<#{tag2}><#{tag1}>Phrase modifiers</#{tag1}></#{tag2}>"
57 57 assert_html_output text => html
58 58 end
59 59 end
60 60 end
61 61
62 62 def test_styles
63 63 # single style
64 64 assert_html_output({
65 65 'p{color:red}. text' => '<p style="color:red;">text</p>',
66 66 'p{color:red;}. text' => '<p style="color:red;">text</p>',
67 67 'p{color: red}. text' => '<p style="color: red;">text</p>',
68 68 'p{color:#f00}. text' => '<p style="color:#f00;">text</p>',
69 69 'p{color:#ff0000}. text' => '<p style="color:#ff0000;">text</p>',
70 70 'p{border:10px}. text' => '<p style="border:10px;">text</p>',
71 71 'p{border:10}. text' => '<p style="border:10;">text</p>',
72 72 'p{border:10%}. text' => '<p style="border:10%;">text</p>',
73 73 'p{border:10em}. text' => '<p style="border:10em;">text</p>',
74 74 'p{border:1.5em}. text' => '<p style="border:1.5em;">text</p>',
75 75 'p{border-left:1px}. text' => '<p style="border-left:1px;">text</p>',
76 76 'p{border-right:1px}. text' => '<p style="border-right:1px;">text</p>',
77 77 'p{border-top:1px}. text' => '<p style="border-top:1px;">text</p>',
78 78 'p{border-bottom:1px}. text' => '<p style="border-bottom:1px;">text</p>',
79 79 }, false)
80 80
81 81 # multiple styles
82 82 assert_html_output({
83 83 'p{color:red; border-top:1px}. text' => '<p style="color:red;border-top:1px;">text</p>',
84 84 'p{color:red ; border-top:1px}. text' => '<p style="color:red;border-top:1px;">text</p>',
85 85 'p{color:red;border-top:1px}. text' => '<p style="color:red;border-top:1px;">text</p>',
86 86 }, false)
87 87
88 88 # styles with multiple values
89 89 assert_html_output({
90 90 'p{border:1px solid red;}. text' => '<p style="border:1px solid red;">text</p>',
91 91 'p{border-top-left-radius: 10px 5px;}. text' => '<p style="border-top-left-radius: 10px 5px;">text</p>',
92 92 }, false)
93 93 end
94 94
95 95 def test_invalid_styles_should_be_filtered
96 96 assert_html_output({
97 97 'p{invalid}. text' => '<p>text</p>',
98 98 'p{invalid:red}. text' => '<p>text</p>',
99 99 'p{color:(red)}. text' => '<p>text</p>',
100 100 'p{color:red;invalid:blue}. text' => '<p style="color:red;">text</p>',
101 101 'p{invalid:blue;color:red}. text' => '<p style="color:red;">text</p>',
102 'p{color:"}. text' => '<p>text</p>',
102 'p{color:"}. text' => '<p>p{color:"}. text</p>',
103 103 }, false)
104 104 end
105 105
106 106 def test_inline_code
107 107 assert_html_output(
108 108 'this is @some code@' => 'this is <code>some code</code>',
109 109 '@<Location /redmine>@' => '<code>&lt;Location /redmine&gt;</code>'
110 110 )
111 111 end
112 112
113 113 def test_escaping
114 114 assert_html_output(
115 115 'this is a <script>' => 'this is a &lt;script&gt;'
116 116 )
117 117 end
118 118
119 119 def test_use_of_backslashes_followed_by_numbers_in_headers
120 120 assert_html_output({
121 121 'h1. 2009\02\09' => '<h1>2009\02\09</h1>'
122 122 }, false)
123 123 end
124 124
125 125 def test_double_dashes_should_not_strikethrough
126 126 assert_html_output(
127 127 'double -- dashes -- test' => 'double -- dashes -- test',
128 128 'double -- *dashes* -- test' => 'double -- <strong>dashes</strong> -- test'
129 129 )
130 130 end
131 131
132 132 def test_acronyms
133 133 assert_html_output(
134 134 'this is an acronym: GPL(General Public License)' => 'this is an acronym: <acronym title="General Public License">GPL</acronym>',
135 135 '2 letters JP(Jean-Philippe) acronym' => '2 letters <acronym title="Jean-Philippe">JP</acronym> acronym',
136 136 'GPL(This is a double-quoted "title")' => '<acronym title="This is a double-quoted &quot;title&quot;">GPL</acronym>'
137 137 )
138 138 end
139 139
140 140 def test_blockquote
141 141 # orig raw text
142 142 raw = <<-RAW
143 143 John said:
144 144 > Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.
145 145 > Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
146 146 > * Donec odio lorem,
147 147 > * sagittis ac,
148 148 > * malesuada in,
149 149 > * adipiscing eu, dolor.
150 150 >
151 151 > >Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.
152 152 > Proin a tellus. Nam vel neque.
153 153
154 154 He's right.
155 155 RAW
156 156
157 157 # expected html
158 158 expected = <<-EXPECTED
159 159 <p>John said:</p>
160 160 <blockquote>
161 161 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.<br />
162 162 Nullam commodo metus accumsan nulla. Curabitur lobortis dui id dolor.
163 163 <ul>
164 164 <li>Donec odio lorem,</li>
165 165 <li>sagittis ac,</li>
166 166 <li>malesuada in,</li>
167 167 <li>adipiscing eu, dolor.</li>
168 168 </ul>
169 169 <blockquote>
170 170 <p>Nulla varius pulvinar diam. Proin id arcu id lorem scelerisque condimentum. Proin vehicula turpis vitae lacus.</p>
171 171 </blockquote>
172 172 <p>Proin a tellus. Nam vel neque.</p>
173 173 </blockquote>
174 174 <p>He's right.</p>
175 175 EXPECTED
176 176
177 177 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
178 178 end
179 179
180 180 def test_table
181 181 raw = <<-RAW
182 182 This is a table with empty cells:
183 183
184 184 |cell11|cell12||
185 185 |cell21||cell23|
186 186 |cell31|cell32|cell33|
187 187 RAW
188 188
189 189 expected = <<-EXPECTED
190 190 <p>This is a table with empty cells:</p>
191 191
192 192 <table>
193 193 <tr><td>cell11</td><td>cell12</td><td></td></tr>
194 194 <tr><td>cell21</td><td></td><td>cell23</td></tr>
195 195 <tr><td>cell31</td><td>cell32</td><td>cell33</td></tr>
196 196 </table>
197 197 EXPECTED
198 198
199 199 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
200 200 end
201 201
202 202 def test_table_with_line_breaks
203 203 raw = <<-RAW
204 204 This is a table with line breaks:
205 205
206 206 |cell11
207 207 continued|cell12||
208 208 |-cell21-||cell23
209 209 cell23 line2
210 210 cell23 *line3*|
211 211 |cell31|cell32
212 212 cell32 line2|cell33|
213 213
214 214 RAW
215 215
216 216 expected = <<-EXPECTED
217 217 <p>This is a table with line breaks:</p>
218 218
219 219 <table>
220 220 <tr>
221 221 <td>cell11<br />continued</td>
222 222 <td>cell12</td>
223 223 <td></td>
224 224 </tr>
225 225 <tr>
226 226 <td><del>cell21</del></td>
227 227 <td></td>
228 228 <td>cell23<br/>cell23 line2<br/>cell23 <strong>line3</strong></td>
229 229 </tr>
230 230 <tr>
231 231 <td>cell31</td>
232 232 <td>cell32<br/>cell32 line2</td>
233 233 <td>cell33</td>
234 234 </tr>
235 235 </table>
236 236 EXPECTED
237 237
238 238 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
239 239 end
240 240
241 241 def test_textile_should_not_mangle_brackets
242 242 assert_equal '<p>[msg1][msg2]</p>', to_html('[msg1][msg2]')
243 243 end
244 244
245 245 def test_textile_should_escape_image_urls
246 246 # this is onclick="alert('XSS');" in encoded form
247 247 raw = '!/images/comment.png"onclick=&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;&#x27;&#x58;&#x53;&#x53;&#x27;&#x29;;&#x22;!'
248 248 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>'
249 249 assert_equal expected.gsub(%r{\s+}, ''), to_html(raw).gsub(%r{\s+}, '')
250 250 end
251 251
252 252
253 253 STR_WITHOUT_PRE = [
254 254 # 0
255 255 "h1. Title
256 256
257 257 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.",
258 258 # 1
259 259 "h2. Heading 2
260 260
261 261 Maecenas sed elit sit amet mi accumsan vestibulum non nec velit. Proin porta tincidunt lorem, consequat rhoncus dolor fermentum in.
262 262
263 263 Cras ipsum felis, ultrices at porttitor vel, faucibus eu nunc.",
264 264 # 2
265 265 "h2. Heading 2
266 266
267 267 Morbi facilisis accumsan orci non pharetra.
268 268
269 269 h3. Heading 3
270 270
271 271 Nulla nunc nisi, egestas in ornare vel, posuere ac libero.",
272 272 # 3
273 273 "h3. Heading 3
274 274
275 275 Praesent eget turpis nibh, a lacinia nulla.",
276 276 # 4
277 277 "h2. Heading 2
278 278
279 279 Ut rhoncus elementum adipiscing."]
280 280
281 281 TEXT_WITHOUT_PRE = STR_WITHOUT_PRE.join("\n\n").freeze
282 282
283 283 def test_get_section_should_return_the_requested_section_and_its_hash
284 284 assert_section_with_hash STR_WITHOUT_PRE[1], TEXT_WITHOUT_PRE, 2
285 285 assert_section_with_hash STR_WITHOUT_PRE[2..3].join("\n\n"), TEXT_WITHOUT_PRE, 3
286 286 assert_section_with_hash STR_WITHOUT_PRE[3], TEXT_WITHOUT_PRE, 5
287 287 assert_section_with_hash STR_WITHOUT_PRE[4], TEXT_WITHOUT_PRE, 6
288 288
289 289 assert_section_with_hash '', TEXT_WITHOUT_PRE, 0
290 290 assert_section_with_hash '', TEXT_WITHOUT_PRE, 10
291 291 end
292 292
293 293 def test_update_section_should_update_the_requested_section
294 294 replacement = "New text"
295 295
296 296 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)
297 297 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)
298 298 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)
299 299 assert_equal [STR_WITHOUT_PRE[0..3], replacement].flatten.join("\n\n"), @formatter.new(TEXT_WITHOUT_PRE).update_section(6, replacement)
300 300
301 301 assert_equal TEXT_WITHOUT_PRE, @formatter.new(TEXT_WITHOUT_PRE).update_section(0, replacement)
302 302 assert_equal TEXT_WITHOUT_PRE, @formatter.new(TEXT_WITHOUT_PRE).update_section(10, replacement)
303 303 end
304 304
305 305 def test_update_section_with_hash_should_update_the_requested_section
306 306 replacement = "New text"
307 307
308 308 assert_equal [STR_WITHOUT_PRE[0], replacement, STR_WITHOUT_PRE[2..4]].flatten.join("\n\n"),
309 309 @formatter.new(TEXT_WITHOUT_PRE).update_section(2, replacement, Digest::MD5.hexdigest(STR_WITHOUT_PRE[1]))
310 310 end
311 311
312 312 def test_update_section_with_wrong_hash_should_raise_an_error
313 313 assert_raise Redmine::WikiFormatting::StaleSectionError do
314 314 @formatter.new(TEXT_WITHOUT_PRE).update_section(2, "New text", Digest::MD5.hexdigest("Old text"))
315 315 end
316 316 end
317 317
318 318 STR_WITH_PRE = [
319 319 # 0
320 320 "h1. Title
321 321
322 322 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas sed libero.",
323 323 # 1
324 324 "h2. Heading 2
325 325
326 326 <pre><code class=\"ruby\">
327 327 def foo
328 328 end
329 329 </code></pre>
330 330
331 331 <pre><code><pre><code class=\"ruby\">
332 332 Place your code here.
333 333 </code></pre>
334 334 </code></pre>
335 335
336 336 Morbi facilisis accumsan orci non pharetra.
337 337
338 338 <pre>
339 339 Pre Content:
340 340
341 341 h2. Inside pre
342 342
343 343 <tag> inside pre block
344 344
345 345 Morbi facilisis accumsan orci non pharetra.
346 346 </pre>",
347 347 # 2
348 348 "h3. Heading 3
349 349
350 350 Nulla nunc nisi, egestas in ornare vel, posuere ac libero."]
351 351
352 352 def test_get_section_should_ignore_pre_content
353 353 text = STR_WITH_PRE.join("\n\n")
354 354
355 355 assert_section_with_hash STR_WITH_PRE[1..2].join("\n\n"), text, 2
356 356 assert_section_with_hash STR_WITH_PRE[2], text, 3
357 357 end
358 358
359 359 def test_update_section_should_not_escape_pre_content_outside_section
360 360 text = STR_WITH_PRE.join("\n\n")
361 361 replacement = "New text"
362 362
363 363 assert_equal [STR_WITH_PRE[0..1], "New text"].flatten.join("\n\n"),
364 364 @formatter.new(text).update_section(3, replacement)
365 365 end
366 366
367 367 private
368 368
369 369 def assert_html_output(to_test, expect_paragraph = true)
370 370 to_test.each do |text, expected|
371 371 assert_equal(( expect_paragraph ? "<p>#{expected}</p>" : expected ), @formatter.new(text).to_html, "Formatting the following text failed:\n===\n#{text}\n===\n")
372 372 end
373 373 end
374 374
375 375 def to_html(text)
376 376 @formatter.new(text).to_html
377 377 end
378 378
379 379 def assert_section_with_hash(expected, text, index)
380 380 result = @formatter.new(text).get_section(index)
381 381
382 382 assert_kind_of Array, result
383 383 assert_equal 2, result.size
384 384 assert_equal expected, result.first, "section content did not match"
385 385 assert_equal Digest::MD5.hexdigest(expected), result.last, "section hash did not match"
386 386 end
387 387 end
General Comments 0
You need to be logged in to leave comments. Login now