BlueCloth 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/ruby
2
+ #
3
+ # Module Install Script
4
+ # $Id: install.rb 11 2005-08-07 03:30:22Z ged $
5
+ #
6
+ # Thanks to Masatoshi SEKI for ideas found in his install.rb.
7
+ #
8
+ # Copyright (c) 2001-2005 The FaerieMUD Consortium.
9
+ #
10
+ # This is free software. You may use, modify, and/or redistribute this
11
+ # software under the terms of the Perl Artistic License. (See
12
+ # http://language.perl.com/misc/Artistic.html)
13
+ #
14
+
15
+ require './utils.rb'
16
+ include UtilityFunctions
17
+
18
+ require 'rbconfig'
19
+ include Config
20
+
21
+ require 'find'
22
+ require 'ftools'
23
+ require 'optparse'
24
+
25
+ $version = %q$Revision: 11 $
26
+ $rcsId = %q$Id: install.rb 11 2005-08-07 03:30:22Z ged $
27
+
28
+ # Define required libraries
29
+ RequiredLibraries = [
30
+ # libraryname, nice name, RAA URL, Download URL, e.g.,
31
+ #[ 'strscan', "Strscan",
32
+ # 'http://www.ruby-lang.org/en/raa-list.rhtml?name=strscan',
33
+ # 'http://i.loveruby.net/archive/strscan/strscan-0.6.7.tar.gz',
34
+ #],
35
+ ]
36
+
37
+ class Installer
38
+
39
+ @@PrunePatterns = [
40
+ /CVS/,
41
+ /~$/,
42
+ %r:(^|/)\.:,
43
+ /\.tpl$/,
44
+ ]
45
+
46
+ def initialize( testing=false )
47
+ @ftools = (testing) ? self : File
48
+ end
49
+
50
+ ### Make the specified dirs (which can be a String or an Array of Strings)
51
+ ### with the specified mode.
52
+ def makedirs( dirs, mode=0755, verbose=false )
53
+ dirs = [ dirs ] unless dirs.is_a? Array
54
+
55
+ oldumask = File::umask
56
+ File::umask( 0777 - mode )
57
+
58
+ for dir in dirs
59
+ if @ftools == File
60
+ File::mkpath( dir, $verbose )
61
+ else
62
+ $stderr.puts "Make path %s with mode %o" % [ dir, mode ]
63
+ end
64
+ end
65
+
66
+ File::umask( oldumask )
67
+ end
68
+
69
+ def install( srcfile, dstfile, mode=nil, verbose=false )
70
+ dstfile = File.catname(srcfile, dstfile)
71
+ unless FileTest.exist? dstfile and File.cmp srcfile, dstfile
72
+ $stderr.puts " install #{srcfile} -> #{dstfile}"
73
+ else
74
+ $stderr.puts " skipping #{dstfile}: unchanged"
75
+ end
76
+ end
77
+
78
+ public
79
+
80
+ def installFiles( src, dstDir, mode=0444, verbose=false )
81
+ directories = []
82
+ files = []
83
+
84
+ if File.directory?( src )
85
+ Find.find( src ) {|f|
86
+ Find.prune if @@PrunePatterns.find {|pat| f =~ pat}
87
+ next if f == src
88
+
89
+ if FileTest.directory?( f )
90
+ directories << f.gsub( /^#{src}#{File::Separator}/, '' )
91
+ next
92
+
93
+ elsif FileTest.file?( f )
94
+ files << f.gsub( /^#{src}#{File::Separator}/, '' )
95
+
96
+ else
97
+ Find.prune
98
+ end
99
+ }
100
+ else
101
+ files << File.basename( src )
102
+ src = File.dirname( src )
103
+ end
104
+
105
+ dirs = [ dstDir ]
106
+ dirs |= directories.collect {|d| File.join(dstDir,d)}
107
+ makedirs( dirs, 0755, verbose )
108
+ files.each {|f|
109
+ srcfile = File.join(src,f)
110
+ dstfile = File.dirname(File.join( dstDir,f ))
111
+
112
+ if verbose
113
+ if mode
114
+ $stderr.puts "Install #{srcfile} -> #{dstfile} (mode %o)" % mode
115
+ else
116
+ $stderr.puts "Install #{srcfile} -> #{dstfile}"
117
+ end
118
+ end
119
+
120
+ @ftools.install( srcfile, dstfile, mode, verbose )
121
+ }
122
+ end
123
+
124
+ end
125
+
126
+
127
+ if $0 == __FILE__
128
+ dryrun = false
129
+
130
+ # Parse command-line switches
131
+ ARGV.options {|oparser|
132
+ oparser.banner = "Usage: #$0 [options]\n"
133
+
134
+ oparser.on( "--verbose", "-v", TrueClass, "Make progress verbose" ) {
135
+ $VERBOSE = true
136
+ debugMsg "Turned verbose on."
137
+ }
138
+
139
+ oparser.on( "--dry-run", "-n", TrueClass, "Don't really install anything" ) {
140
+ debugMsg "Turned dry-run on."
141
+ dryrun = true
142
+ }
143
+
144
+ # Handle the 'help' option
145
+ oparser.on( "--help", "-h", "Display this text." ) {
146
+ $stderr.puts oparser
147
+ exit!(0)
148
+ }
149
+
150
+ oparser.parse!
151
+ }
152
+
153
+ # Don't do anything if they expect this to be the three-step install script
154
+ # and they aren't doing the 'install' step.
155
+ if ARGV.include?( "config" )
156
+ for lib in RequiredLibraries
157
+ testForRequiredLibrary( *lib )
158
+ end
159
+ puts "Done."
160
+ elsif ARGV.include?( "setup" )
161
+ puts "Done."
162
+ elsif ARGV.empty?
163
+ for lib in RequiredLibraries
164
+ testForRequiredLibrary( *lib )
165
+ end
166
+ end
167
+
168
+ if ARGV.empty? || ARGV.include?( "install" )
169
+ debugMsg "Sitelibdir = '#{CONFIG['sitelibdir']}'"
170
+ sitelibdir = CONFIG['sitelibdir']
171
+ debugMsg "Sitearchdir = '#{CONFIG['sitearchdir']}'"
172
+ sitearchdir = CONFIG['sitearchdir']
173
+
174
+ message "Installing..."
175
+ i = Installer.new( dryrun )
176
+ #i.installFiles( "redist", sitelibdir, 0444, verbose )
177
+ i.installFiles( "lib", sitelibdir, 0444, $VERBOSE )
178
+
179
+ message "done.\n"
180
+ end
181
+ end
182
+
183
+
184
+
185
+
@@ -0,0 +1,1144 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'digest/md5'
4
+ require 'logger'
5
+ require 'strscan'
6
+
7
+ # Bluecloth is a Ruby implementation of Markdown, a text-to-HTML conversion
8
+ # tool.
9
+ #
10
+ # == Synopsis
11
+ #
12
+ # doc = BlueCloth::new "
13
+ # ## Test document ##
14
+ #
15
+ # Just a simple test.
16
+ # "
17
+ #
18
+ # puts doc.to_html
19
+ #
20
+ # == Authors
21
+ #
22
+ # * Michael Granger <ged@FaerieMUD.org>
23
+ #
24
+ # == Contributors
25
+ #
26
+ # * Martin Chase <stillflame@FaerieMUD.org> - Peer review, helpful suggestions
27
+ # * Florian Gross <flgr@ccan.de> - Filter options, suggestions
28
+ #
29
+ # == Copyright
30
+ #
31
+ # Original version:
32
+ # Copyright (c) 2003-2004 John Gruber
33
+ # <http://daringfireball.net/>
34
+ # All rights reserved.
35
+ #
36
+ # Ruby port:
37
+ # Copyright (c) 2004 The FaerieMUD Consortium.
38
+ #
39
+ # BlueCloth is free software; you can redistribute it and/or modify it under the
40
+ # terms of the GNU General Public License as published by the Free Software
41
+ # Foundation; either version 2 of the License, or (at your option) any later
42
+ # version.
43
+ #
44
+ # BlueCloth is distributed in the hope that it will be useful, but WITHOUT ANY
45
+ # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
46
+ # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
47
+ #
48
+ # == To-do
49
+ #
50
+ # * Refactor some of the larger uglier methods that have to do their own
51
+ # brute-force scanning because of lack of Perl features in Ruby's Regexp
52
+ # class. Alternately, could add a dependency on 'pcre' and use most Perl
53
+ # regexps.
54
+ #
55
+ # * Put the StringScanner in the render state for thread-safety.
56
+ #
57
+ # == Version
58
+ #
59
+ # $Id: bluecloth.rb 130 2009-07-16 00:08:36Z deveiant $
60
+ #
61
+ class BlueCloth < String
62
+
63
+ ### Exception class for formatting errors.
64
+ class FormatError < RuntimeError
65
+
66
+ ### Create a new FormatError with the given source +str+ and an optional
67
+ ### message about the +specific+ error.
68
+ def initialize( str, specific=nil )
69
+ if specific
70
+ msg = "Bad markdown format near %p: %s" % [ str, specific ]
71
+ else
72
+ msg = "Bad markdown format near %p" % str
73
+ end
74
+
75
+ super( msg )
76
+ end
77
+ end
78
+
79
+
80
+ # Release Version
81
+ Version = VERSION = '1.0.1'
82
+
83
+ # SVN Revision
84
+ SvnRev = %q$Rev: 130 $
85
+
86
+ # SVN Id tag
87
+ SvnId = %q$Id: bluecloth.rb 130 2009-07-16 00:08:36Z deveiant $
88
+
89
+ # SVN URL
90
+ SvnUrl = %q$URL: svn+ssh://deveiate/svn/BlueCloth/releases/1.0.0/lib/bluecloth.rb $
91
+
92
+
93
+ # Rendering state struct. Keeps track of URLs, titles, and HTML blocks
94
+ # midway through a render. I prefer this to the globals of the Perl version
95
+ # because globals make me break out in hives. Or something.
96
+ RenderState = Struct::new( "RenderState", :urls, :titles, :html_blocks, :log )
97
+
98
+ # Tab width for #detab! if none is specified
99
+ TabWidth = 4
100
+
101
+ # The tag-closing string -- set to '>' for HTML
102
+ EmptyElementSuffix = "/>";
103
+
104
+ # Table of MD5 sums for escaped characters
105
+ EscapeTable = {}
106
+ '\\`*_{}[]()#.!'.split(//).each {|char|
107
+ hash = Digest::MD5::hexdigest( char )
108
+
109
+ EscapeTable[ char ] = {
110
+ :md5 => hash,
111
+ :md5re => Regexp::new( hash ),
112
+ :re => Regexp::new( '\\\\' + Regexp::escape(char) ),
113
+ }
114
+ }
115
+
116
+
117
+ #################################################################
118
+ ### I N S T A N C E M E T H O D S
119
+ #################################################################
120
+
121
+ ### Create a new BlueCloth string.
122
+ def initialize( content="", *restrictions )
123
+ @log = Logger::new( $deferr )
124
+ @log.level = $DEBUG ?
125
+ Logger::DEBUG :
126
+ ($VERBOSE ? Logger::INFO : Logger::WARN)
127
+ @scanner = nil
128
+
129
+ # Add any restrictions, and set the line-folding attribute to reflect
130
+ # what happens by default.
131
+ @filter_html = nil
132
+ @filter_styles = nil
133
+ restrictions.flatten.each {|r| __send__("#{r}=", true) }
134
+ @fold_lines = true
135
+
136
+ super( content )
137
+
138
+ @log.debug "String is: %p" % self
139
+ end
140
+
141
+
142
+ ######
143
+ public
144
+ ######
145
+
146
+ # Filters for controlling what gets output for untrusted input. (But really,
147
+ # you're filtering bad stuff out of untrusted input at submission-time via
148
+ # untainting, aren't you?)
149
+ attr_accessor :filter_html, :filter_styles
150
+
151
+ # RedCloth-compatibility accessor. Line-folding is part of Markdown syntax,
152
+ # so this isn't used by anything.
153
+ attr_accessor :fold_lines
154
+
155
+
156
+ ### Render Markdown-formatted text in this string object as HTML and return
157
+ ### it. The parameter is for compatibility with RedCloth, and is currently
158
+ ### unused, though that may change in the future.
159
+ def to_html( lite=false )
160
+
161
+ # Create a StringScanner we can reuse for various lexing tasks
162
+ @scanner = StringScanner::new( '' )
163
+
164
+ # Make a structure to carry around stuff that gets placeholdered out of
165
+ # the source.
166
+ rs = RenderState::new( {}, {}, {} )
167
+
168
+ # Make a copy of the string with normalized line endings, tabs turned to
169
+ # spaces, and a couple of guaranteed newlines at the end
170
+ text = self.gsub( /\r\n?/, "\n" ).detab
171
+ text += "\n\n"
172
+ @log.debug "Normalized line-endings: %p" % text
173
+
174
+ # Filter HTML if we're asked to do so
175
+ if self.filter_html
176
+ text.gsub!( "<", "&lt;" )
177
+ text.gsub!( ">", "&gt;" )
178
+ @log.debug "Filtered HTML: %p" % text
179
+ end
180
+
181
+ # Simplify blank lines
182
+ text.gsub!( /^ +$/, '' )
183
+ @log.debug "Tabs -> spaces/blank lines stripped: %p" % text
184
+
185
+ # Replace HTML blocks with placeholders
186
+ text = hide_html_blocks( text, rs )
187
+ @log.debug "Hid HTML blocks: %p" % text
188
+ @log.debug "Render state: %p" % rs
189
+
190
+ # Strip link definitions, store in render state
191
+ text = strip_link_definitions( text, rs )
192
+ @log.debug "Stripped link definitions: %p" % text
193
+ @log.debug "Render state: %p" % rs
194
+
195
+ # Escape meta-characters
196
+ text = escape_special_chars( text )
197
+ @log.debug "Escaped special characters: %p" % text
198
+
199
+ # Transform block-level constructs
200
+ text = apply_block_transforms( text, rs )
201
+ @log.debug "After block-level transforms: %p" % text
202
+
203
+ # Now swap back in all the escaped characters
204
+ text = unescape_special_chars( text )
205
+ @log.debug "After unescaping special characters: %p" % text
206
+
207
+ return text
208
+ end
209
+
210
+
211
+ ### Convert tabs in +str+ to spaces.
212
+ def detab( tabwidth=TabWidth )
213
+ copy = self.dup
214
+ copy.detab!( tabwidth )
215
+ return copy
216
+ end
217
+
218
+
219
+ ### Convert tabs to spaces in place and return self if any were converted.
220
+ def detab!( tabwidth=TabWidth )
221
+ newstr = self.split( /\n/ ).collect {|line|
222
+ line.gsub( /(.*?)\t/ ) do
223
+ $1 + ' ' * (tabwidth - $1.length % tabwidth)
224
+ end
225
+ }.join("\n")
226
+ self.replace( newstr )
227
+ end
228
+
229
+
230
+ #######
231
+ #private
232
+ #######
233
+
234
+ ### Do block-level transforms on a copy of +str+ using the specified render
235
+ ### state +rs+ and return the results.
236
+ def apply_block_transforms( str, rs )
237
+ # Port: This was called '_runBlockGamut' in the original
238
+
239
+ @log.debug "Applying block transforms to:\n %p" % str
240
+ text = transform_headers( str, rs )
241
+ text = transform_hrules( text, rs )
242
+ text = transform_lists( text, rs )
243
+ text = transform_code_blocks( text, rs )
244
+ text = transform_block_quotes( text, rs )
245
+ text = transform_auto_links( text, rs )
246
+ text = hide_html_blocks( text, rs )
247
+
248
+ text = form_paragraphs( text, rs )
249
+
250
+ @log.debug "Done with block transforms:\n %p" % text
251
+ return text
252
+ end
253
+
254
+
255
+ ### Apply Markdown span transforms to a copy of the specified +str+ with the
256
+ ### given render state +rs+ and return it.
257
+ def apply_span_transforms( str, rs )
258
+ @log.debug "Applying span transforms to:\n %p" % str
259
+
260
+ str = transform_code_spans( str, rs )
261
+ str = encode_html( str )
262
+ str = transform_images( str, rs )
263
+ str = transform_anchors( str, rs )
264
+ str = transform_italic_and_bold( str, rs )
265
+
266
+ # Hard breaks
267
+ str.gsub!( / {2,}\n/, "<br#{EmptyElementSuffix}\n" )
268
+
269
+ @log.debug "Done with span transforms:\n %p" % str
270
+ return str
271
+ end
272
+
273
+
274
+ # The list of tags which are considered block-level constructs and an
275
+ # alternation pattern suitable for use in regexps made from the list
276
+ StrictBlockTags = %w[ p div h[1-6] blockquote pre table dl ol ul script noscript
277
+ form fieldset iframe math ins del ]
278
+ StrictTagPattern = StrictBlockTags.join('|')
279
+
280
+ LooseBlockTags = StrictBlockTags - %w[ins del]
281
+ LooseTagPattern = LooseBlockTags.join('|')
282
+
283
+ # Nested blocks:
284
+ # <div>
285
+ # <div>
286
+ # tags for inner block must be indented.
287
+ # </div>
288
+ # </div>
289
+ StrictBlockRegex = %r{
290
+ ^ # Start of line
291
+ <(#{StrictTagPattern}) # Start tag: \2
292
+ \b # word break
293
+ (.*\n)*? # Any number of lines, minimal match
294
+ </\1> # Matching end tag
295
+ [ ]* # trailing spaces
296
+ $ # End of line or document
297
+ }ix
298
+
299
+ # More-liberal block-matching
300
+ LooseBlockRegex = %r{
301
+ ^ # Start of line
302
+ <(#{LooseTagPattern}) # start tag: \2
303
+ \b # word break
304
+ (.*\n)*? # Any number of lines, minimal match
305
+ .*</\1> # Anything + Matching end tag
306
+ [ ]* # trailing spaces
307
+ $ # End of line or document
308
+ }ix
309
+
310
+ # Special case for <hr />.
311
+ HruleBlockRegex = %r{
312
+ ( # $1
313
+ \A\n? # Start of doc + optional \n
314
+ | # or
315
+ .*\n\n # anything + blank line
316
+ )
317
+ ( # save in $2
318
+ [ ]* # Any spaces
319
+ <hr # Tag open
320
+ \b # Word break
321
+ ([^<>])*? # Attributes
322
+ /?> # Tag close
323
+ $ # followed by a blank line or end of document
324
+ )
325
+ }ix
326
+
327
+ ### Replace all blocks of HTML in +str+ that start in the left margin with
328
+ ### tokens.
329
+ def hide_html_blocks( str, rs )
330
+ @log.debug "Hiding HTML blocks in %p" % str
331
+
332
+ # Tokenizer proc to pass to gsub
333
+ tokenize = lambda {|match|
334
+ key = Digest::MD5::hexdigest( match )
335
+ rs.html_blocks[ key ] = match
336
+ @log.debug "Replacing %p with %p" % [ match, key ]
337
+ "\n\n#{key}\n\n"
338
+ }
339
+
340
+ rval = str.dup
341
+
342
+ @log.debug "Finding blocks with the strict regex..."
343
+ rval.gsub!( StrictBlockRegex, &tokenize )
344
+
345
+ @log.debug "Finding blocks with the loose regex..."
346
+ rval.gsub!( LooseBlockRegex, &tokenize )
347
+
348
+ @log.debug "Finding hrules..."
349
+ rval.gsub!( HruleBlockRegex ) {|match| $1 + tokenize[$2] }
350
+
351
+ return rval
352
+ end
353
+
354
+
355
+ # Link defs are in the form: ^[id]: url "optional title"
356
+ LinkRegex = %r{
357
+ ^[ ]*\[(.+)\]: # id = $1
358
+ [ ]*
359
+ \n? # maybe *one* newline
360
+ [ ]*
361
+ <?(\S+?)>? # url = $2
362
+ [ ]*
363
+ \n? # maybe one newline
364
+ [ ]*
365
+ (?:
366
+ # Titles are delimited by "quotes" or (parens).
367
+ ["(]
368
+ (.+?) # title = $3
369
+ [")] # Matching ) or "
370
+ [ ]*
371
+ )? # title is optional
372
+ (?:\n+|\Z)
373
+ }x
374
+
375
+ ### Strip link definitions from +str+, storing them in the given RenderState
376
+ ### +rs+.
377
+ def strip_link_definitions( str, rs )
378
+ str.gsub( LinkRegex ) {|match|
379
+ id, url, title = $1, $2, $3
380
+
381
+ rs.urls[ id.downcase ] = encode_html( url )
382
+ unless title.nil?
383
+ rs.titles[ id.downcase ] = title.gsub( /"/, "&quot;" )
384
+ end
385
+ ""
386
+ }
387
+ end
388
+
389
+
390
+ ### Escape special characters in the given +str+
391
+ def escape_special_chars( str )
392
+ @log.debug " Escaping special characters"
393
+ text = ''
394
+
395
+ # The original Markdown source has something called '$tags_to_skip'
396
+ # declared here, but it's never used, so I don't define it.
397
+
398
+ tokenize_html( str ) {|token, str|
399
+ @log.debug " Adding %p token %p" % [ token, str ]
400
+ case token
401
+
402
+ # Within tags, encode * and _
403
+ when :tag
404
+ text += str.
405
+ gsub( /\*/, EscapeTable['*'][:md5] ).
406
+ gsub( /_/, EscapeTable['_'][:md5] )
407
+
408
+ # Encode backslashed stuff in regular text
409
+ when :text
410
+ text += encode_backslash_escapes( str )
411
+ else
412
+ raise TypeError, "Unknown token type %p" % token
413
+ end
414
+ }
415
+
416
+ @log.debug " Text with escapes is now: %p" % text
417
+ return text
418
+ end
419
+
420
+
421
+ ### Swap escaped special characters in a copy of the given +str+ and return
422
+ ### it.
423
+ def unescape_special_chars( str )
424
+ EscapeTable.each {|char, hash|
425
+ @log.debug "Unescaping escaped %p with %p" % [ char, hash[:md5re] ]
426
+ str.gsub!( hash[:md5re], char )
427
+ }
428
+
429
+ return str
430
+ end
431
+
432
+
433
+ ### Return a copy of the given +str+ with any backslashed special character
434
+ ### in it replaced with MD5 placeholders.
435
+ def encode_backslash_escapes( str )
436
+ # Make a copy with any double-escaped backslashes encoded
437
+ text = str.gsub( /\\\\/, EscapeTable['\\'][:md5] )
438
+
439
+ EscapeTable.each_pair {|char, esc|
440
+ next if char == '\\'
441
+ text.gsub!( esc[:re], esc[:md5] )
442
+ }
443
+
444
+ return text
445
+ end
446
+
447
+
448
+ ### Transform any Markdown-style horizontal rules in a copy of the specified
449
+ ### +str+ and return it.
450
+ def transform_hrules( str, rs )
451
+ @log.debug " Transforming horizontal rules"
452
+ str.gsub( /^ ?([\-\*_] ?){3,}$/, "\n<hr#{EmptyElementSuffix}\n" )
453
+ end
454
+
455
+
456
+
457
+ # Patterns to match and transform lists
458
+ ListMarkerOl = %r{\d+\.}
459
+ ListMarkerUl = %r{[*+-]}
460
+ ListMarkerAny = Regexp::union( ListMarkerOl, ListMarkerUl )
461
+
462
+ ListRegexp = %r{
463
+ (?:
464
+ ^[ ]{0,#{TabWidth - 1}} # Indent < tab width
465
+ (#{ListMarkerAny}) # unordered or ordered ($1)
466
+ [ ]+ # At least one space
467
+ )
468
+ (?m:.+?) # item content (include newlines)
469
+ (?:
470
+ \z # Either EOF
471
+ | # or
472
+ \n{2,} # Blank line...
473
+ (?=\S) # ...followed by non-space
474
+ (?![ ]* # ...but not another item
475
+ (#{ListMarkerAny})
476
+ [ ]+)
477
+ )
478
+ }x
479
+
480
+ ### Transform Markdown-style lists in a copy of the specified +str+ and
481
+ ### return it.
482
+ def transform_lists( str, rs )
483
+ @log.debug " Transforming lists at %p" % (str[0,100] + '...')
484
+
485
+ str.gsub( ListRegexp ) {|list|
486
+ @log.debug " Found list %p" % list
487
+ bullet = $1
488
+ list_type = (ListMarkerUl.match(bullet) ? "ul" : "ol")
489
+ list.gsub!( /\n{2,}/, "\n\n\n" )
490
+
491
+ %{<%s>\n%s</%s>\n} % [
492
+ list_type,
493
+ transform_list_items( list, rs ),
494
+ list_type,
495
+ ]
496
+ }
497
+ end
498
+
499
+
500
+ # Pattern for transforming list items
501
+ ListItemRegexp = %r{
502
+ (\n)? # leading line = $1
503
+ (^[ ]*) # leading whitespace = $2
504
+ (#{ListMarkerAny}) [ ]+ # list marker = $3
505
+ ((?m:.+?) # list item text = $4
506
+ (\n{1,2}))
507
+ (?= \n* (\z | \2 (#{ListMarkerAny}) [ ]+))
508
+ }x
509
+
510
+ ### Transform list items in a copy of the given +str+ and return it.
511
+ def transform_list_items( str, rs )
512
+ @log.debug " Transforming list items"
513
+
514
+ # Trim trailing blank lines
515
+ str = str.sub( /\n{2,}\z/, "\n" )
516
+
517
+ str.gsub( ListItemRegexp ) {|line|
518
+ @log.debug " Found item line %p" % line
519
+ leading_line, item = $1, $4
520
+
521
+ if leading_line or /\n{2,}/.match( item )
522
+ @log.debug " Found leading line or item has a blank"
523
+ item = apply_block_transforms( outdent(item), rs )
524
+ else
525
+ # Recursion for sub-lists
526
+ @log.debug " Recursing for sublist"
527
+ item = transform_lists( outdent(item), rs ).chomp
528
+ item = apply_span_transforms( item, rs )
529
+ end
530
+
531
+ %{<li>%s</li>\n} % item
532
+ }
533
+ end
534
+
535
+
536
+ # Pattern for matching codeblocks
537
+ CodeBlockRegexp = %r{
538
+ (?:\n\n|\A)
539
+ ( # $1 = the code block
540
+ (?:
541
+ (?:[ ]{#{TabWidth}} | \t) # a tab or tab-width of spaces
542
+ .*\n+
543
+ )+
544
+ )
545
+ (^[ ]{0,#{TabWidth - 1}}\S|\Z) # Lookahead for non-space at
546
+ # line-start, or end of doc
547
+ }x
548
+
549
+ ### Transform Markdown-style codeblocks in a copy of the specified +str+ and
550
+ ### return it.
551
+ def transform_code_blocks( str, rs )
552
+ @log.debug " Transforming code blocks"
553
+
554
+ str.gsub( CodeBlockRegexp ) {|block|
555
+ codeblock = $1
556
+ remainder = $2
557
+
558
+ # Generate the codeblock
559
+ %{\n\n<pre><code>%s\n</code></pre>\n\n%s} %
560
+ [ encode_code( outdent(codeblock), rs ).rstrip, remainder ]
561
+ }
562
+ end
563
+
564
+
565
+ # Pattern for matching Markdown blockquote blocks
566
+ BlockQuoteRegexp = %r{
567
+ (?:
568
+ ^[ ]*>[ ]? # '>' at the start of a line
569
+ .+\n # rest of the first line
570
+ (?:.+\n)* # subsequent consecutive lines
571
+ \n* # blanks
572
+ )+
573
+ }x
574
+ PreChunk = %r{ ( ^ \s* <pre> .+? </pre> ) }xm
575
+
576
+ ### Transform Markdown-style blockquotes in a copy of the specified +str+
577
+ ### and return it.
578
+ def transform_block_quotes( str, rs )
579
+ @log.debug " Transforming block quotes"
580
+
581
+ str.gsub( BlockQuoteRegexp ) {|quote|
582
+ @log.debug "Making blockquote from %p" % quote
583
+
584
+ quote.gsub!( /^ *> ?/, '' ) # Trim one level of quoting
585
+ quote.gsub!( /^ +$/, '' ) # Trim whitespace-only lines
586
+
587
+ indent = " " * TabWidth
588
+ quoted = %{<blockquote>\n%s\n</blockquote>\n\n} %
589
+ apply_block_transforms( quote, rs ).
590
+ gsub( /^/, indent ).
591
+ gsub( PreChunk ) {|m| m.gsub(/^#{indent}/o, '') }
592
+ @log.debug "Blockquoted chunk is: %p" % quoted
593
+ quoted
594
+ }
595
+ end
596
+
597
+
598
+ AutoAnchorURLRegexp = /<((https?|ftp):[^'">\s]+)>/
599
+ AutoAnchorEmailRegexp = %r{
600
+ <
601
+ (
602
+ [-.\w]+
603
+ \@
604
+ [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+
605
+ )
606
+ >
607
+ }xi
608
+
609
+ ### Transform URLs in a copy of the specified +str+ into links and return
610
+ ### it.
611
+ def transform_auto_links( str, rs )
612
+ @log.debug " Transforming auto-links"
613
+ str.gsub( AutoAnchorURLRegexp, %{<a href="\\1">\\1</a>}).
614
+ gsub( AutoAnchorEmailRegexp ) {|addr|
615
+ encode_email_address( unescape_special_chars($1) )
616
+ }
617
+ end
618
+
619
+
620
+ # Encoder functions to turn characters of an email address into encoded
621
+ # entities.
622
+ Encoders = [
623
+ lambda {|char| "&#%03d;" % char},
624
+ lambda {|char| "&#x%X;" % char},
625
+ lambda {|char| char.chr },
626
+ ]
627
+
628
+ ### Transform a copy of the given email +addr+ into an escaped version safer
629
+ ### for posting publicly.
630
+ def encode_email_address( addr )
631
+
632
+ rval = ''
633
+ ("mailto:" + addr).each_byte {|b|
634
+ case b
635
+ when ?:
636
+ rval += ":"
637
+ when ?@
638
+ rval += Encoders[ rand(2) ][ b ]
639
+ else
640
+ r = rand(100)
641
+ rval += (
642
+ r > 90 ? Encoders[2][ b ] :
643
+ r < 45 ? Encoders[1][ b ] :
644
+ Encoders[0][ b ]
645
+ )
646
+ end
647
+ }
648
+
649
+ return %{<a href="%s">%s</a>} % [ rval, rval.sub(/.+?:/, '') ]
650
+ end
651
+
652
+
653
+ # Regex for matching Setext-style headers
654
+ SetextHeaderRegexp = %r{
655
+ (.+) # The title text ($1)
656
+ \n
657
+ ([\-=])+ # Match a line of = or -. Save only one in $2.
658
+ [ ]*\n+
659
+ }x
660
+
661
+ # Regexp for matching ATX-style headers
662
+ AtxHeaderRegexp = %r{
663
+ ^(\#{1,6}) # $1 = string of #'s
664
+ [ ]*
665
+ (.+?) # $2 = Header text
666
+ [ ]*
667
+ \#* # optional closing #'s (not counted)
668
+ \n+
669
+ }x
670
+
671
+ ### Apply Markdown header transforms to a copy of the given +str+ amd render
672
+ ### state +rs+ and return the result.
673
+ def transform_headers( str, rs )
674
+ @log.debug " Transforming headers"
675
+
676
+ # Setext-style headers:
677
+ # Header 1
678
+ # ========
679
+ #
680
+ # Header 2
681
+ # --------
682
+ #
683
+ str.
684
+ gsub( SetextHeaderRegexp ) {|m|
685
+ @log.debug "Found setext-style header"
686
+ title, hdrchar = $1, $2
687
+ title = apply_span_transforms( title, rs )
688
+
689
+ case hdrchar
690
+ when '='
691
+ %[<h1>#{title}</h1>\n\n]
692
+ when '-'
693
+ %[<h2>#{title}</h2>\n\n]
694
+ else
695
+ title
696
+ end
697
+ }.
698
+
699
+ gsub( AtxHeaderRegexp ) {|m|
700
+ @log.debug "Found ATX-style header"
701
+ hdrchars, title = $1, $2
702
+ title = apply_span_transforms( title, rs )
703
+
704
+ level = hdrchars.length
705
+ %{<h%d>%s</h%d>\n\n} % [ level, title, level ]
706
+ }
707
+ end
708
+
709
+
710
+ ### Wrap all remaining paragraph-looking text in a copy of +str+ inside <p>
711
+ ### tags and return it.
712
+ def form_paragraphs( str, rs )
713
+ @log.debug " Forming paragraphs"
714
+ grafs = str.
715
+ sub( /\A\n+/, '' ).
716
+ sub( /\n+\z/, '' ).
717
+ split( /\n{2,}/ )
718
+
719
+ rval = grafs.collect {|graf|
720
+
721
+ # Unhashify HTML blocks if this is a placeholder
722
+ if rs.html_blocks.key?( graf )
723
+ rs.html_blocks[ graf ]
724
+
725
+ # Otherwise, wrap in <p> tags
726
+ else
727
+ apply_span_transforms(graf, rs).
728
+ sub( /^[ ]*/, '<p>' ) + '</p>'
729
+ end
730
+ }.join( "\n\n" )
731
+
732
+ @log.debug " Formed paragraphs: %p" % rval
733
+ return rval
734
+ end
735
+
736
+
737
+ # Pattern to match the linkid part of an anchor tag for reference-style
738
+ # links.
739
+ RefLinkIdRegex = %r{
740
+ [ ]? # Optional leading space
741
+ (?:\n[ ]*)? # Optional newline + spaces
742
+ \[
743
+ (.*?) # Id = $1
744
+ \]
745
+ }x
746
+
747
+ InlineLinkRegex = %r{
748
+ \( # Literal paren
749
+ [ ]* # Zero or more spaces
750
+ <?(.+?)>? # URI = $1
751
+ [ ]* # Zero or more spaces
752
+ (?: #
753
+ ([\"\']) # Opening quote char = $2
754
+ (.*?) # Title = $3
755
+ \2 # Matching quote char
756
+ )? # Title is optional
757
+ \)
758
+ }x
759
+
760
+ ### Apply Markdown anchor transforms to a copy of the specified +str+ with
761
+ ### the given render state +rs+ and return it.
762
+ def transform_anchors( str, rs )
763
+ @log.debug " Transforming anchors"
764
+ @scanner.string = str.dup
765
+ text = ''
766
+
767
+ # Scan the whole string
768
+ until @scanner.empty?
769
+
770
+ if @scanner.scan( /\[/ )
771
+ link = ''; linkid = ''
772
+ depth = 1
773
+ startpos = @scanner.pos
774
+ @log.debug " Found a bracket-open at %d" % startpos
775
+
776
+ # Scan the rest of the tag, allowing unlimited nested []s. If
777
+ # the scanner runs out of text before the opening bracket is
778
+ # closed, append the text and return (wasn't a valid anchor).
779
+ while depth.nonzero?
780
+ linktext = @scanner.scan_until( /\]|\[/ )
781
+
782
+ if linktext
783
+ @log.debug " Found a bracket at depth %d: %p" % [ depth, linktext ]
784
+ link += linktext
785
+
786
+ # Decrement depth for each closing bracket
787
+ depth += ( linktext[-1, 1] == ']' ? -1 : 1 )
788
+ @log.debug " Depth is now #{depth}"
789
+
790
+ # If there's no more brackets, it must not be an anchor, so
791
+ # just abort.
792
+ else
793
+ @log.debug " Missing closing brace, assuming non-link."
794
+ link += @scanner.rest
795
+ @scanner.terminate
796
+ return text + '[' + link
797
+ end
798
+ end
799
+ link.slice!( -1 ) # Trim final ']'
800
+ @log.debug " Found leading link %p" % link
801
+
802
+ # Look for a reference-style second part
803
+ if @scanner.scan( RefLinkIdRegex )
804
+ linkid = @scanner[1]
805
+ linkid = link.dup if linkid.empty?
806
+ linkid.downcase!
807
+ @log.debug " Found a linkid: %p" % linkid
808
+
809
+ # If there's a matching link in the link table, build an
810
+ # anchor tag for it.
811
+ if rs.urls.key?( linkid )
812
+ @log.debug " Found link key in the link table: %p" % rs.urls[linkid]
813
+ url = escape_md( rs.urls[linkid] )
814
+
815
+ text += %{<a href="#{url}"}
816
+ if rs.titles.key?(linkid)
817
+ text += %{ title="%s"} % escape_md( rs.titles[linkid] )
818
+ end
819
+ text += %{>#{link}</a>}
820
+
821
+ # If the link referred to doesn't exist, just append the raw
822
+ # source to the result
823
+ else
824
+ @log.debug " Linkid %p not found in link table" % linkid
825
+ @log.debug " Appending original string instead: "
826
+ @log.debug "%p" % @scanner.string[ startpos-1 .. @scanner.pos-1 ]
827
+ text += @scanner.string[ startpos-1 .. @scanner.pos-1 ]
828
+ end
829
+
830
+ # ...or for an inline style second part
831
+ elsif @scanner.scan( InlineLinkRegex )
832
+ url = @scanner[1]
833
+ title = @scanner[3]
834
+ @log.debug " Found an inline link to %p" % url
835
+
836
+ text += %{<a href="%s"} % escape_md( url )
837
+ if title
838
+ title.gsub!( /"/, "&quot;" )
839
+ text += %{ title="%s"} % escape_md( title )
840
+ end
841
+ text += %{>#{link}</a>}
842
+
843
+ # No linkid part: just append the first part as-is.
844
+ else
845
+ @log.debug "No linkid, so no anchor. Appending literal text."
846
+ text += @scanner.string[ startpos-1 .. @scanner.pos-1 ]
847
+ end # if linkid
848
+
849
+ # Plain text
850
+ else
851
+ @log.debug " Scanning to the next link from %p" % @scanner.rest
852
+ text += @scanner.scan( /[^\[]+/ )
853
+ end
854
+
855
+ end # until @scanner.empty?
856
+
857
+ return text
858
+ end
859
+
860
+
861
+ # Pattern to match strong emphasis in Markdown text
862
+ BoldRegexp = %r{ (\*\*|__) (\S|\S.*?\S) \1 }x
863
+
864
+ # Pattern to match normal emphasis in Markdown text
865
+ ItalicRegexp = %r{ (\*|_) (\S|\S.*?\S) \1 }x
866
+
867
+ ### Transform italic- and bold-encoded text in a copy of the specified +str+
868
+ ### and return it.
869
+ def transform_italic_and_bold( str, rs )
870
+ @log.debug " Transforming italic and bold"
871
+
872
+ str.
873
+ gsub( BoldRegexp, %{<strong>\\2</strong>} ).
874
+ gsub( ItalicRegexp, %{<em>\\2</em>} )
875
+ end
876
+
877
+
878
+ ### Transform backticked spans into <code> spans.
879
+ def transform_code_spans( str, rs )
880
+ @log.debug " Transforming code spans"
881
+
882
+ # Set up the string scanner and just return the string unless there's at
883
+ # least one backtick.
884
+ @scanner.string = str.dup
885
+ unless @scanner.exist?( /`/ )
886
+ @scanner.terminate
887
+ @log.debug "No backticks found for code span in %p" % str
888
+ return str
889
+ end
890
+
891
+ @log.debug "Transforming code spans in %p" % str
892
+
893
+ # Build the transformed text anew
894
+ text = ''
895
+
896
+ # Scan to the end of the string
897
+ until @scanner.empty?
898
+
899
+ # Scan up to an opening backtick
900
+ if pre = @scanner.scan_until( /.?(?=`)/m )
901
+ text += pre
902
+ @log.debug "Found backtick at %d after '...%s'" % [ @scanner.pos, text[-10, 10] ]
903
+
904
+ # Make a pattern to find the end of the span
905
+ opener = @scanner.scan( /`+/ )
906
+ len = opener.length
907
+ closer = Regexp::new( opener )
908
+ @log.debug "Scanning for end of code span with %p" % closer
909
+
910
+ # Scan until the end of the closing backtick sequence. Chop the
911
+ # backticks off the resultant string, strip leading and trailing
912
+ # whitespace, and encode any enitites contained in it.
913
+ codespan = @scanner.scan_until( closer ) or
914
+ raise FormatError::new( @scanner.rest[0,20],
915
+ "No %p found before end" % opener )
916
+
917
+ @log.debug "Found close of code span at %d: %p" % [ @scanner.pos - len, codespan ]
918
+ codespan.slice!( -len, len )
919
+ text += "<code>%s</code>" %
920
+ encode_code( codespan.strip, rs )
921
+
922
+ # If there's no more backticks, just append the rest of the string
923
+ # and move the scan pointer to the end
924
+ else
925
+ text += @scanner.rest
926
+ @scanner.terminate
927
+ end
928
+ end
929
+
930
+ return text
931
+ end
932
+
933
+
934
+ # Next, handle inline images: ![alt text](url "optional title")
935
+ # Don't forget: encode * and _
936
+ InlineImageRegexp = %r{
937
+ ( # Whole match = $1
938
+ !\[ (.*?) \] # alt text = $2
939
+ \([ ]*
940
+ <?(\S+?)>? # source url = $3
941
+ [ ]*
942
+ (?: #
943
+ (["']) # quote char = $4
944
+ (.*?) # title = $5
945
+ \4 # matching quote
946
+ [ ]*
947
+ )? # title is optional
948
+ \)
949
+ )
950
+ }xs #"
951
+
952
+
953
+ # Reference-style images
954
+ ReferenceImageRegexp = %r{
955
+ ( # Whole match = $1
956
+ !\[ (.*?) \] # Alt text = $2
957
+ [ ]? # Optional space
958
+ (?:\n[ ]*)? # One optional newline + spaces
959
+ \[ (.*?) \] # id = $3
960
+ )
961
+ }xs
962
+
963
+ ### Turn image markup into image tags.
964
+ def transform_images( str, rs )
965
+ @log.debug " Transforming images: %p" % [ str ]
966
+
967
+ # Handle reference-style labeled images: ![alt text][id]
968
+ str.
969
+ gsub( ReferenceImageRegexp ) {|match|
970
+ whole, alt, linkid = $1, $2, $3.downcase
971
+ @log.debug "Matched %p" % match
972
+ res = nil
973
+ alt.gsub!( /"/, '&quot;' )
974
+
975
+ # for shortcut links like ![this][].
976
+ linkid = alt.downcase if linkid.empty?
977
+
978
+ if rs.urls.key?( linkid )
979
+ url = escape_md( rs.urls[linkid] )
980
+ @log.debug "Found url '%s' for linkid '%s' " % [ url, linkid ]
981
+
982
+ # Build the tag
983
+ result = %{<img src="%s" alt="%s"} % [ url, alt ]
984
+ if rs.titles.key?( linkid )
985
+ result += %{ title="%s"} % escape_md( rs.titles[linkid] )
986
+ end
987
+ result += EmptyElementSuffix
988
+
989
+ else
990
+ result = whole
991
+ end
992
+
993
+ @log.debug "Replacing %p with %p" % [ match, result ]
994
+ result
995
+ }.
996
+
997
+ # Inline image style
998
+ gsub( InlineImageRegexp ) {|match|
999
+ @log.debug "Found inline image %p" % match
1000
+ whole, alt, title = $1, $2, $5
1001
+ url = escape_md( $3 )
1002
+ alt.gsub!( /"/, '&quot;' )
1003
+
1004
+ # Build the tag
1005
+ result = %{<img src="%s" alt="%s"} % [ url, alt ]
1006
+ unless title.nil?
1007
+ title.gsub!( /"/, '&quot;' )
1008
+ result += %{ title="%s"} % escape_md( title )
1009
+ end
1010
+ result += EmptyElementSuffix
1011
+
1012
+ @log.debug "Replacing %p with %p" % [ match, result ]
1013
+ result
1014
+ }
1015
+ end
1016
+
1017
+
1018
+ # Regexp to match special characters in a code block
1019
+ CodeEscapeRegexp = %r{( \* | _ | \{ | \} | \[ | \] | \\ )}x
1020
+
1021
+ ### Escape any characters special to HTML and encode any characters special
1022
+ ### to Markdown in a copy of the given +str+ and return it.
1023
+ def encode_code( str, rs )
1024
+ str.gsub( %r{&}, '&amp;' ).
1025
+ gsub( %r{<}, '&lt;' ).
1026
+ gsub( %r{>}, '&gt;' ).
1027
+ gsub( CodeEscapeRegexp ) {|match| EscapeTable[match][:md5]}
1028
+ end
1029
+
1030
+
1031
+
1032
+ #################################################################
1033
+ ### U T I L I T Y F U N C T I O N S
1034
+ #################################################################
1035
+
1036
+ ### Escape any markdown characters in a copy of the given +str+ and return
1037
+ ### it.
1038
+ def escape_md( str )
1039
+ str.
1040
+ gsub( /\*/, EscapeTable['*'][:md5] ).
1041
+ gsub( /_/, EscapeTable['_'][:md5] )
1042
+ end
1043
+
1044
+
1045
+ # Matching constructs for tokenizing X/HTML
1046
+ HTMLCommentRegexp = %r{ <! ( -- .*? -- \s* )+ > }mx
1047
+ XMLProcInstRegexp = %r{ <\? .*? \?> }mx
1048
+ MetaTag = Regexp::union( HTMLCommentRegexp, XMLProcInstRegexp )
1049
+
1050
+ HTMLTagOpenRegexp = %r{ < [a-z/!$] [^<>]* }imx
1051
+ HTMLTagCloseRegexp = %r{ > }x
1052
+ HTMLTagPart = Regexp::union( HTMLTagOpenRegexp, HTMLTagCloseRegexp )
1053
+
1054
+ ### Break the HTML source in +str+ into a series of tokens and return
1055
+ ### them. The tokens are just 2-element Array tuples with a type and the
1056
+ ### actual content. If this function is called with a block, the type and
1057
+ ### text parts of each token will be yielded to it one at a time as they are
1058
+ ### extracted.
1059
+ def tokenize_html( str )
1060
+ depth = 0
1061
+ tokens = []
1062
+ @scanner.string = str.dup
1063
+ type, token = nil, nil
1064
+
1065
+ until @scanner.empty?
1066
+ @log.debug "Scanning from %p" % @scanner.rest
1067
+
1068
+ # Match comments and PIs without nesting
1069
+ if (( token = @scanner.scan(MetaTag) ))
1070
+ type = :tag
1071
+
1072
+ # Do nested matching for HTML tags
1073
+ elsif (( token = @scanner.scan(HTMLTagOpenRegexp) ))
1074
+ tagstart = @scanner.pos
1075
+ @log.debug " Found the start of a plain tag at %d" % tagstart
1076
+
1077
+ # Start the token with the opening angle
1078
+ depth = 1
1079
+ type = :tag
1080
+
1081
+ # Scan the rest of the tag, allowing unlimited nested <>s. If
1082
+ # the scanner runs out of text before the tag is closed, raise
1083
+ # an error.
1084
+ while depth.nonzero?
1085
+
1086
+ # Scan either an opener or a closer
1087
+ chunk = @scanner.scan( HTMLTagPart ) or
1088
+ raise "Malformed tag at character %d: %p" %
1089
+ [ tagstart, token + @scanner.rest ]
1090
+
1091
+ @log.debug " Found another part of the tag at depth %d: %p" % [ depth, chunk ]
1092
+
1093
+ token += chunk
1094
+
1095
+ # If the last character of the token so far is a closing
1096
+ # angle bracket, decrement the depth. Otherwise increment
1097
+ # it for a nested tag.
1098
+ depth += ( token[-1, 1] == '>' ? -1 : 1 )
1099
+ @log.debug " Depth is now #{depth}"
1100
+ end
1101
+
1102
+ # Match text segments
1103
+ else
1104
+ @log.debug " Looking for a chunk of text"
1105
+ type = :text
1106
+
1107
+ # Scan forward, always matching at least one character to move
1108
+ # the pointer beyond any non-tag '<'.
1109
+ token = @scanner.scan_until( /[^<]+/m )
1110
+ end
1111
+
1112
+ @log.debug " type: %p, token: %p" % [ type, token ]
1113
+
1114
+ # If a block is given, feed it one token at a time. Add the token to
1115
+ # the token list to be returned regardless.
1116
+ if block_given?
1117
+ yield( type, token )
1118
+ end
1119
+ tokens << [ type, token ]
1120
+ end
1121
+
1122
+ return tokens
1123
+ end
1124
+
1125
+
1126
+ ### Return a copy of +str+ with angle brackets and ampersands HTML-encoded.
1127
+ def encode_html( str )
1128
+ str.gsub( /&(?!#?[x]?(?:[0-9a-f]+|\w+);)/i, "&amp;" ).
1129
+ gsub( %r{<(?![a-z/?\$!])}i, "&lt;" )
1130
+ end
1131
+
1132
+
1133
+ ### Return one level of line-leading tabs or spaces from a copy of +str+ and
1134
+ ### return it.
1135
+ def outdent( str )
1136
+ str.gsub( /^(\t|[ ]{1,#{TabWidth}})/, '')
1137
+ end
1138
+
1139
+ end # class BlueCloth
1140
+
1141
+
1142
+ # Set the top-level 'Markdown' constant.
1143
+ ::Markdown = ::BlueCloth unless defined?( ::Markdown )
1144
+