kramdown 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of kramdown might be problematic. Click here for more details.

Files changed (44) hide show
  1. data/ChangeLog +276 -0
  2. data/Rakefile +2 -2
  3. data/VERSION +1 -1
  4. data/benchmark/benchmark.rb +1 -1
  5. data/benchmark/historic-jruby-1.4.0.dat +7 -7
  6. data/benchmark/historic-ruby-1.8.6.dat +7 -7
  7. data/benchmark/historic-ruby-1.8.7.dat +7 -7
  8. data/benchmark/historic-ruby-1.9.1p243.dat +7 -7
  9. data/benchmark/historic-ruby-1.9.2dev.dat +7 -7
  10. data/bin/kramdown +46 -1
  11. data/doc/index.page +2 -2
  12. data/doc/syntax.page +7 -3
  13. data/lib/kramdown/converter.rb +6 -285
  14. data/lib/kramdown/converter/base.rb +75 -0
  15. data/lib/kramdown/converter/html.rb +325 -0
  16. data/lib/kramdown/converter/latex.rb +516 -0
  17. data/lib/kramdown/document.rb +36 -66
  18. data/lib/kramdown/options.rb +262 -0
  19. data/lib/kramdown/parser/kramdown.rb +36 -17
  20. data/lib/kramdown/parser/kramdown/attribute_list.rb +1 -1
  21. data/lib/kramdown/parser/kramdown/autolink.rb +1 -1
  22. data/lib/kramdown/parser/kramdown/codespan.rb +1 -1
  23. data/lib/kramdown/parser/kramdown/emphasis.rb +2 -2
  24. data/lib/kramdown/parser/kramdown/escaped_chars.rb +2 -2
  25. data/lib/kramdown/parser/kramdown/extension.rb +46 -2
  26. data/lib/kramdown/parser/kramdown/footnote.rb +1 -1
  27. data/lib/kramdown/parser/kramdown/html.rb +2 -2
  28. data/lib/kramdown/parser/kramdown/html_entity.rb +4 -5
  29. data/lib/kramdown/parser/kramdown/line_break.rb +1 -1
  30. data/lib/kramdown/parser/kramdown/link.rb +2 -2
  31. data/lib/kramdown/parser/kramdown/smart_quotes.rb +213 -0
  32. data/lib/kramdown/parser/kramdown/typographic_symbol.rb +1 -1
  33. data/lib/kramdown/version.rb +1 -1
  34. data/test/testcases/encoding.html +46 -0
  35. data/test/testcases/encoding.text +28 -0
  36. data/test/testcases/span/01_link/inline.html +1 -1
  37. data/test/testcases/span/01_link/reference.html +2 -2
  38. data/test/testcases/span/escaped_chars/normal.html +4 -0
  39. data/test/testcases/span/escaped_chars/normal.text +4 -0
  40. data/test/testcases/span/text_substitutions/entities.html +1 -1
  41. data/test/testcases/span/text_substitutions/typography.html +12 -0
  42. data/test/testcases/span/text_substitutions/typography.text +12 -0
  43. metadata +9 -3
  44. data/lib/kramdown/extension.rb +0 -98
data/doc/syntax.page CHANGED
@@ -7,9 +7,7 @@ This is version **<%= ::Kramdown::VERSION %>** of the syntax documentation.
7
7
 
8
8
  Table of Contents:
9
9
 
10
- {::nomarkdown:}
11
10
  {menu: {used_nodes: fragments, min_levels: 3}}
12
- {::nomarkdown:}
13
11
 
14
12
  # kramdown Syntax
15
13
 
@@ -80,6 +78,8 @@ Following is a list of all those characters (character sequences) that can be es
80
78
  >> right guillemet
81
79
  : colon
82
80
  | pipe
81
+ " double quote
82
+ ' single quote
83
83
 
84
84
 
85
85
  ## Typographic Symbols
@@ -94,6 +94,10 @@ kramdown converts the following plain ASCII character into their corresponding t
94
94
  * `>>` will become a right guillemet (like this >>) -- an optional leading space will become a
95
95
  non-breakable space
96
96
 
97
+ It also replaces normal single `'` and double quotes `"` with "fancy quotes". There *may* be times
98
+ when kramdown falsely replace the quotes. If this is the case, just \'escape\" the quotes and they
99
+ won't be replaced with fancy ones.
100
+
97
101
  > The original Markdown program does not do this transformations.
98
102
  {: .markdown-difference}
99
103
 
@@ -1367,7 +1371,7 @@ single space, for example:
1367
1371
  Link: <a href="some
1368
1372
  link">text</a>
1369
1373
  ^
1370
- <p>Link: <a href="some link">text</a>
1374
+ <p>Link: <a href="some link">text</a></p>
1371
1375
 
1372
1376
 
1373
1377
  ## Footnotes
@@ -20,296 +20,17 @@
20
20
  #++
21
21
  #
22
22
 
23
- require 'rexml/parsers/baseparser'
24
-
25
23
  module Kramdown
26
24
 
27
25
  # This module contains all available converters, i.e. classes that take a document and convert the
28
- # document tree to a string in a specific format, for example, HTML.
26
+ # document tree to a string in a specific format, for example, HTML. These converters use the Base
27
+ # class for common functionality - see its API documentation for how to create a converter class.
29
28
  module Converter
30
29
 
31
- # Converts a Kramdown::Document to HTML.
32
- class Html
33
-
34
- INDENTATION = 2
35
-
36
- begin
37
- require 'coderay'
38
- HIGHLIGHTING_AVAILABLE = true
39
- rescue LoadError => e
40
- HIGHLIGHTING_AVAILABLE = false
41
- end
42
-
43
- # Initialize the HTML converter with the given Kramdown document +doc+.
44
- def initialize(doc)
45
- @doc = doc
46
- @footnote_counter = @footnote_start = @doc.options[:footnote_nr]
47
- @footnotes = []
48
- end
49
- private_class_method(:new, :allocate)
50
-
51
- # Convert the Kramdown document +doc+ to HTML.
52
- def self.convert(doc)
53
- new(doc).convert(doc.tree)
54
- end
55
-
56
- # Convert the element tree +el+, setting the indentation level to +indent+.
57
- def convert(el, indent = -INDENTATION, opts = {})
58
- send("convert_#{el.type}", el, indent, opts)
59
- end
60
-
61
- def inner(el, indent, opts)
62
- result = ''
63
- indent += INDENTATION
64
- el.children.each do |inner_el|
65
- result << send("convert_#{inner_el.type}", inner_el, indent, opts)
66
- end
67
- result
68
- end
69
-
70
- def convert_blank(el, indent, opts)
71
- "\n"
72
- end
73
-
74
- def convert_text(el, indent, opts)
75
- escape_html(el.value, false)
76
- end
77
-
78
- def convert_eob(el, indent, opts)
79
- ''
80
- end
81
-
82
- def convert_p(el, indent, opts)
83
- "#{' '*indent}<p#{options_for_element(el)}>#{inner(el, indent, opts)}</p>\n"
84
- end
85
-
86
- def convert_codeblock(el, indent, opts)
87
- if el.options[:attr] && el.options[:attr]['lang'] && HIGHLIGHTING_AVAILABLE && @doc.options[:coderay]
88
- el = Marshal.load(Marshal.dump(el)) # so that the original is not changed
89
- result = CodeRay.scan(el.value, el.options[:attr].delete('lang').to_sym).html(@doc.options[:coderay]).chomp + "\n"
90
- "#{' '*indent}<div#{options_for_element(el)}>#{result}#{' '*indent}</div>\n"
91
- else
92
- result = escape_html(el.value)
93
- if el.options[:attr] && el.options[:attr].has_key?('class') && el.options[:attr]['class'] =~ /\bshow-whitespaces\b/
94
- result.gsub!(/(?:(^[ \t]+)|([ \t]+$)|([ \t]+))/) do |m|
95
- suffix = ($1 ? '-l' : ($2 ? '-r' : ''))
96
- m.scan(/./).map do |c|
97
- case c
98
- when "\t" then "<span class=\"ws-tab#{suffix}\">\t</span>"
99
- when " " then "<span class=\"ws-space#{suffix}\">&sdot;</span>"
100
- end
101
- end.join('')
102
- end
103
- end
104
- "#{' '*indent}<pre#{options_for_element(el)}><code>#{result}#{result =~ /\n\Z/ ? '' : "\n"}</code></pre>\n"
105
- end
106
- end
107
-
108
- def convert_blockquote(el, indent, opts)
109
- "#{' '*indent}<blockquote#{options_for_element(el)}>\n#{inner(el, indent, opts)}#{' '*indent}</blockquote>\n"
110
- end
111
-
112
- def convert_header(el, indent, opts)
113
- "#{' '*indent}<h#{el.options[:level]}#{options_for_element(el)}>#{inner(el, indent, opts)}</h#{el.options[:level]}>\n"
114
- end
115
-
116
- def convert_hr(el, indent, opts)
117
- "#{' '*indent}<hr />\n"
118
- end
119
-
120
- def convert_ul(el, indent, opts)
121
- "#{' '*indent}<#{el.type}#{options_for_element(el)}>\n#{inner(el, indent, opts)}#{' '*indent}</#{el.type}>\n"
122
- end
123
- alias :convert_ol :convert_ul
124
- alias :convert_dl :convert_ul
125
-
126
- def convert_li(el, indent, opts)
127
- output = ' '*indent << "<#{el.type}" << options_for_element(el) << ">"
128
- res = inner(el, indent, opts)
129
- if el.options[:first_is_block]
130
- output << "\n" << res << ' '*indent
131
- else
132
- output << res << (res =~ /\n\Z/ ? ' '*indent : '')
133
- end
134
- output << "</#{el.type}>\n"
135
- end
136
- alias :convert_dd :convert_li
137
-
138
- def convert_dt(el, indent, opts)
139
- "#{' '*indent}<dt#{options_for_element(el)}>#{inner(el, indent, opts)}</dt>\n"
140
- end
141
-
142
- HTML_TAGS_WITH_BODY=['div', 'script']
143
-
144
- def convert_html_element(el, indent, opts)
145
- res = inner(el, indent, opts)
146
- if @doc.options[:filter_html].include?(el.value)
147
- res.chomp + (el.options[:type] == :block ? "\n" : '')
148
- elsif el.options[:type] == :span
149
- "<#{el.value}#{options_for_element(el)}" << (!res.empty? ? ">#{res}</#{el.value}>" : " />")
150
- else
151
- output = ''
152
- output << ' '*indent if el.options[:parse_type] != :raw && !el.options[:parent_is_raw]
153
- output << "<#{el.value}#{options_for_element(el)}"
154
- if !res.empty? && el.options[:parse_type] != :block
155
- output << ">#{res}</#{el.value}>"
156
- elsif !res.empty?
157
- output << ">\n#{res}" << ' '*indent << "</#{el.value}>"
158
- elsif HTML_TAGS_WITH_BODY.include?(el.value)
159
- output << "></#{el.value}>"
160
- else
161
- output << " />"
162
- end
163
- output << "\n" if el.options[:outer_element] || (el.options[:parse_type] != :raw && !el.options[:parent_is_raw])
164
- output
165
- end
166
- end
167
-
168
- def convert_html_text(el, indent, opts)
169
- escape_html(el.value, false)
170
- end
171
-
172
- def convert_xml_comment(el, indent, opts)
173
- el.value + (el.options[:type] == :block ? "\n" : '')
174
- end
175
- alias :convert_xml_pi :convert_xml_comment
176
-
177
- def convert_table(el, indent, opts)
178
- if el.options[:alignment].all? {|a| a == :default}
179
- alignment = ''
180
- else
181
- alignment = el.options[:alignment].map do |a|
182
- "#{' '*(indent + INDENTATION)}" + (a == :default ? "<col />" : "<col align=\"#{a}\" />") + "\n"
183
- end.join('')
184
- end
185
- "#{' '*indent}<table#{options_for_element(el)}>\n#{alignment}#{inner(el, indent, opts)}#{' '*indent}</table>\n"
186
- end
187
-
188
- def convert_thead(el, indent, opts)
189
- opts[:cell_type] = case el.type
190
- when :thead then 'th'
191
- when :tbody, :tfoot then 'td'
192
- else opts[:cell_type]
193
- end
194
- "#{' '*indent}<#{el.type}#{options_for_element(el)}>\n#{inner(el, indent, opts)}#{' '*indent}</#{el.type}>\n"
195
- end
196
- alias :convert_tbody :convert_thead
197
- alias :convert_tfoot :convert_thead
198
- alias :convert_tr :convert_thead
199
-
200
- def convert_td(el, indent, opts)
201
- res = inner(el, indent, opts)
202
- "#{' '*indent}<#{opts[:cell_type]}#{options_for_element(el)}>#{res.empty? ? "&nbsp;" : res}</#{opts[:cell_type]}>\n"
203
- end
204
-
205
- def convert_br(el, indent, opts)
206
- "<br />"
207
- end
208
-
209
- def convert_a(el, indent, opts)
210
- if el.options[:attr]['href'] =~ /^mailto:/
211
- el = Marshal.load(Marshal.dump(el)) # so that the original is not changed
212
- href = obfuscate(el.options[:attr]['href'].sub(/^mailto:/, ''))
213
- mailto = obfuscate('mailto')
214
- el.options[:attr]['href'] = "#{mailto}:#{href}"
215
- end
216
- res = inner(el, indent, opts)
217
- res = obfuscate(res) if el.options[:obfuscate_text]
218
- "<a#{options_for_element(el)}>#{res}</a>"
219
- end
220
-
221
- def convert_img(el, indent, opts)
222
- "<img#{options_for_element(el)} />"
223
- end
224
-
225
- def convert_codespan(el, indent, opts)
226
- "<code#{options_for_element(el)}>#{escape_html(el.value)}</code>"
227
- end
228
-
229
- def convert_footnote(el, indent, opts)
230
- number = @footnote_counter
231
- @footnote_counter += 1
232
- @footnotes << [el.options[:name], @doc.parse_infos[:footnotes][el.options[:name]]]
233
- "<sup id=\"fnref:#{el.options[:name]}\"><a href=\"#fn:#{el.options[:name]}\" rel=\"footnote\">#{number}</a></sup>"
234
- end
235
-
236
- def convert_raw(el, indent, opts)
237
- el.value
238
- end
239
-
240
- def convert_em(el, indent, opts)
241
- "<#{el.type}#{options_for_element(el)}>#{inner(el, indent, opts)}</#{el.type}>"
242
- end
243
- alias :convert_strong :convert_em
244
-
245
- def convert_entity(el, indent, opts)
246
- el.value
247
- end
248
-
249
- TYPOGRAPHIC_SYMS = {
250
- :mdash => '&mdash;', :ndash => '&ndash;', :ellipsis => '&hellip;',
251
- :laquo_space => '&laquo;&nbsp;', :raquo_space => '&nbsp;&raquo;',
252
- :laquo => '&laquo;', :raquo => '&raquo;'
253
- }
254
- def convert_typographic_sym(el, indent, opts)
255
- TYPOGRAPHIC_SYMS[el.value]
256
- end
257
-
258
- def convert_root(el, indent, opts)
259
- inner(el, indent, opts) << footnote_content
260
- end
261
-
262
- # Helper method for obfuscating the +text+ by using HTML entities.
263
- def obfuscate(text)
264
- result = ""
265
- text.each_byte do |b|
266
- result += (b > 128 ? b.chr : "&#%03d;" % b)
267
- end
268
- result
269
- end
270
-
271
- # Return a HTML list with the footnote content for the used footnotes.
272
- def footnote_content
273
- ol = Element.new(:ol)
274
- ol.options[:attr] = {'start' => @footnote_start} if @footnote_start != 1
275
- @footnotes.each do |name, data|
276
- li = Element.new(:li, nil, {:attr => {:id => "fn:#{name}"}, :first_is_block => true})
277
- li.children = Marshal.load(Marshal.dump(data[:content].children)) #TODO: probably remove this!!!!
278
- ol.children << li
279
-
280
- ref = Element.new(:raw, "<a href=\"#fnref:#{name}\" rev=\"footnote\">&#8617;</a>")
281
- if li.children.last.type == :p
282
- para = li.children.last
283
- else
284
- li.children << (para = Element.new(:p))
285
- end
286
- para.children << ref
287
- end
288
- (ol.children.empty? ? '' : "<div class=\"footnotes\">\n#{convert(ol, 2)}</div>\n")
289
- end
290
-
291
- # Return the string with the attributes of the element +el+.
292
- def options_for_element(el)
293
- (el.options[:attr] || {}).map {|k,v| v.nil? ? '' : " #{k}=\"#{escape_html(v.to_s, false)}\"" }.sort.join('')
294
- end
295
-
296
- ESCAPE_MAP = {
297
- '<' => '&lt;',
298
- '>' => '&gt;',
299
- '"' => '&quot;',
300
- '&' => '&amp;'
301
- }
302
- ESCAPE_ALL_RE = Regexp.union(*ESCAPE_MAP.collect {|k,v| Regexp.escape(k)})
303
- ESCAPE_ALL_NOT_ENTITIES_RE = Regexp.union(REXML::Parsers::BaseParser::REFERENCE_RE, ESCAPE_ALL_RE)
304
-
305
- # Escape the special HTML characters in the string +str+. If +all+ is +true+ then all
306
- # characters are escaped, if +all+ is +false+ then only those characters are escaped that are
307
- # not part on an HTML entity.
308
- def escape_html(str, all = true)
309
- str.gsub(all ? ESCAPE_ALL_RE : ESCAPE_ALL_NOT_ENTITIES_RE) {|m| ESCAPE_MAP[m] || m}
310
- end
311
-
312
- end
30
+ autoload :Base, 'kramdown/converter/base'
31
+ autoload :Html, 'kramdown/converter/html'
32
+ autoload :Latex, 'kramdown/converter/latex'
313
33
 
314
34
  end
35
+
315
36
  end
@@ -0,0 +1,75 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ #--
4
+ # Copyright (C) 2009 Thomas Leitner <t_leitner@gmx.at>
5
+ #
6
+ # This file is part of kramdown.
7
+ #
8
+ # kramdown is free software: you can redistribute it and/or modify
9
+ # it under the terms of the GNU General Public License as published by
10
+ # the Free Software Foundation, either version 3 of the License, or
11
+ # (at your option) any later version.
12
+ #
13
+ # This program is distributed in the hope that it will be useful,
14
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ # GNU General Public License for more details.
17
+ #
18
+ # You should have received a copy of the GNU General Public License
19
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
20
+ #++
21
+ #
22
+
23
+ require 'erb'
24
+
25
+ module Kramdown
26
+
27
+ module Converter
28
+
29
+ # This class servers as base class for all converters.
30
+ class Base
31
+
32
+ # Initialize the converter with the given Kramdown document +doc+.
33
+ def initialize(doc)
34
+ @doc = doc
35
+ @doc.conversion_infos.clear
36
+ end
37
+ private_class_method(:new, :allocate)
38
+
39
+ # Convert the Kramdown document +doc+ to the output format implemented by a subclass.
40
+ #
41
+ # Initializes a new instance of the calling class and then calls the #convert method that must
42
+ # be implemented by each subclass. If the +template+ option is specified and non-empty, the
43
+ # result is rendered into the specified template.
44
+ def self.convert(doc)
45
+ result = new(doc).convert(doc.tree)
46
+ result = apply_template(doc, result) if !doc.options[:template].empty?
47
+ result
48
+ end
49
+
50
+ # Apply the template specified in the +doc+ options, using +body+ as the body string.
51
+ def self.apply_template(doc, body)
52
+ erb = ERB.new(get_template(doc.options[:template]))
53
+ erb.result(binding)
54
+ end
55
+
56
+ # Return the template specified by +template+.
57
+ def self.get_template(template)
58
+ format_ext = '.' + self.name.split(/::/).last.downcase
59
+ shipped = File.join(Kramdown.data_dir, template + format_ext)
60
+ if File.exist?(template)
61
+ File.read(template)
62
+ elsif File.exist?(template + format_ext)
63
+ File.read(template + format_ext)
64
+ elsif File.exist?(shipped)
65
+ File.read(shipped)
66
+ else
67
+ raise "The specified template file #{template} does not exist"
68
+ end
69
+ end
70
+
71
+ end
72
+
73
+ end
74
+
75
+ end
@@ -0,0 +1,325 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ #--
4
+ # Copyright (C) 2009 Thomas Leitner <t_leitner@gmx.at>
5
+ #
6
+ # This file is part of kramdown.
7
+ #
8
+ # kramdown is free software: you can redistribute it and/or modify
9
+ # it under the terms of the GNU General Public License as published by
10
+ # the Free Software Foundation, either version 3 of the License, or
11
+ # (at your option) any later version.
12
+ #
13
+ # This program is distributed in the hope that it will be useful,
14
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ # GNU General Public License for more details.
17
+ #
18
+ # You should have received a copy of the GNU General Public License
19
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
20
+ #++
21
+ #
22
+
23
+ require 'rexml/parsers/baseparser'
24
+
25
+ module Kramdown
26
+
27
+ module Converter
28
+
29
+ # Converts a Kramdown::Document to HTML.
30
+ class Html < Base
31
+
32
+ # :stopdoc:
33
+
34
+ # Defines the amount of indentation used when nesting HTML tags.
35
+ INDENTATION = 2
36
+
37
+ begin
38
+ require 'coderay'
39
+
40
+ # Highlighting via coderay is available if this constant is +true+.
41
+ HIGHLIGHTING_AVAILABLE = true
42
+ rescue LoadError => e
43
+ HIGHLIGHTING_AVAILABLE = false
44
+ end
45
+
46
+ # Initialize the HTML converter with the given Kramdown document +doc+.
47
+ def initialize(doc)
48
+ super
49
+ @footnote_counter = @footnote_start = @doc.options[:footnote_nr]
50
+ @footnotes = []
51
+ end
52
+
53
+ def convert(el, indent = -INDENTATION, opts = {})
54
+ send("convert_#{el.type}", el, indent, opts)
55
+ end
56
+
57
+ def inner(el, indent, opts)
58
+ result = ''
59
+ indent += INDENTATION
60
+ el.children.each do |inner_el|
61
+ result << send("convert_#{inner_el.type}", inner_el, indent, opts)
62
+ end
63
+ result
64
+ end
65
+
66
+ def convert_blank(el, indent, opts)
67
+ "\n"
68
+ end
69
+
70
+ def convert_text(el, indent, opts)
71
+ escape_html(el.value, :text)
72
+ end
73
+
74
+ def convert_eob(el, indent, opts)
75
+ ''
76
+ end
77
+
78
+ def convert_p(el, indent, opts)
79
+ "#{' '*indent}<p#{options_for_element(el)}>#{inner(el, indent, opts)}</p>\n"
80
+ end
81
+
82
+ def convert_codeblock(el, indent, opts)
83
+ if el.options[:attr] && el.options[:attr]['lang'] && HIGHLIGHTING_AVAILABLE
84
+ el = Marshal.load(Marshal.dump(el)) # so that the original is not changed
85
+ opts = {:wrap => @doc.options[:coderay_wrap], :line_numbers => @doc.options[:coderay_line_numbers],
86
+ :line_number_start => @doc.options[:coderay_line_number_start], :tab_width => @doc.options[:coderay_tab_width],
87
+ :bold_every => @doc.options[:coderay_bold_every], :css => @doc.options[:coderay_css]}
88
+ result = CodeRay.scan(el.value, el.options[:attr].delete('lang').to_sym).html(opts).chomp + "\n"
89
+ "#{' '*indent}<div#{options_for_element(el)}>#{result}#{' '*indent}</div>\n"
90
+ else
91
+ result = escape_html(el.value)
92
+ if el.options[:attr] && el.options[:attr].has_key?('class') && el.options[:attr]['class'] =~ /\bshow-whitespaces\b/
93
+ result.gsub!(/(?:(^[ \t]+)|([ \t]+$)|([ \t]+))/) do |m|
94
+ suffix = ($1 ? '-l' : ($2 ? '-r' : ''))
95
+ m.scan(/./).map do |c|
96
+ case c
97
+ when "\t" then "<span class=\"ws-tab#{suffix}\">\t</span>"
98
+ when " " then "<span class=\"ws-space#{suffix}\">&sdot;</span>"
99
+ end
100
+ end.join('')
101
+ end
102
+ end
103
+ "#{' '*indent}<pre#{options_for_element(el)}><code>#{result}#{result =~ /\n\Z/ ? '' : "\n"}</code></pre>\n"
104
+ end
105
+ end
106
+
107
+ def convert_blockquote(el, indent, opts)
108
+ "#{' '*indent}<blockquote#{options_for_element(el)}>\n#{inner(el, indent, opts)}#{' '*indent}</blockquote>\n"
109
+ end
110
+
111
+ def convert_header(el, indent, opts)
112
+ "#{' '*indent}<h#{el.options[:level]}#{options_for_element(el)}>#{inner(el, indent, opts)}</h#{el.options[:level]}>\n"
113
+ end
114
+
115
+ def convert_hr(el, indent, opts)
116
+ "#{' '*indent}<hr />\n"
117
+ end
118
+
119
+ def convert_ul(el, indent, opts)
120
+ "#{' '*indent}<#{el.type}#{options_for_element(el)}>\n#{inner(el, indent, opts)}#{' '*indent}</#{el.type}>\n"
121
+ end
122
+ alias :convert_ol :convert_ul
123
+ alias :convert_dl :convert_ul
124
+
125
+ def convert_li(el, indent, opts)
126
+ output = ' '*indent << "<#{el.type}" << options_for_element(el) << ">"
127
+ res = inner(el, indent, opts)
128
+ if el.options[:first_is_block]
129
+ output << "\n" << res << ' '*indent
130
+ else
131
+ output << res << (res =~ /\n\Z/ ? ' '*indent : '')
132
+ end
133
+ output << "</#{el.type}>\n"
134
+ end
135
+ alias :convert_dd :convert_li
136
+
137
+ def convert_dt(el, indent, opts)
138
+ "#{' '*indent}<dt#{options_for_element(el)}>#{inner(el, indent, opts)}</dt>\n"
139
+ end
140
+
141
+ HTML_TAGS_WITH_BODY=['div', 'script']
142
+
143
+ def convert_html_element(el, indent, opts)
144
+ res = inner(el, indent, opts)
145
+ if @doc.options[:filter_html].include?(el.value)
146
+ res.chomp + (el.options[:type] == :block ? "\n" : '')
147
+ elsif el.options[:type] == :span
148
+ "<#{el.value}#{options_for_element(el)}" << (!res.empty? ? ">#{res}</#{el.value}>" : " />")
149
+ else
150
+ output = ''
151
+ output << ' '*indent if el.options[:parse_type] != :raw && !el.options[:parent_is_raw]
152
+ output << "<#{el.value}#{options_for_element(el)}"
153
+ if !res.empty? && el.options[:parse_type] != :block
154
+ output << ">#{res}</#{el.value}>"
155
+ elsif !res.empty?
156
+ output << ">\n#{res}" << ' '*indent << "</#{el.value}>"
157
+ elsif HTML_TAGS_WITH_BODY.include?(el.value)
158
+ output << "></#{el.value}>"
159
+ else
160
+ output << " />"
161
+ end
162
+ output << "\n" if el.options[:outer_element] || (el.options[:parse_type] != :raw && !el.options[:parent_is_raw])
163
+ output
164
+ end
165
+ end
166
+
167
+ def convert_html_text(el, indent, opts)
168
+ escape_html(el.value, :text)
169
+ end
170
+
171
+ def convert_xml_comment(el, indent, opts)
172
+ el.value + (el.options[:type] == :block ? "\n" : '')
173
+ end
174
+ alias :convert_xml_pi :convert_xml_comment
175
+
176
+ def convert_table(el, indent, opts)
177
+ if el.options[:alignment].all? {|a| a == :default}
178
+ alignment = ''
179
+ else
180
+ alignment = el.options[:alignment].map do |a|
181
+ "#{' '*(indent + INDENTATION)}" + (a == :default ? "<col />" : "<col align=\"#{a}\" />") + "\n"
182
+ end.join('')
183
+ end
184
+ "#{' '*indent}<table#{options_for_element(el)}>\n#{alignment}#{inner(el, indent, opts)}#{' '*indent}</table>\n"
185
+ end
186
+
187
+ def convert_thead(el, indent, opts)
188
+ opts[:cell_type] = case el.type
189
+ when :thead then 'th'
190
+ when :tbody, :tfoot then 'td'
191
+ else opts[:cell_type]
192
+ end
193
+ "#{' '*indent}<#{el.type}#{options_for_element(el)}>\n#{inner(el, indent, opts)}#{' '*indent}</#{el.type}>\n"
194
+ end
195
+ alias :convert_tbody :convert_thead
196
+ alias :convert_tfoot :convert_thead
197
+ alias :convert_tr :convert_thead
198
+
199
+ def convert_td(el, indent, opts)
200
+ res = inner(el, indent, opts)
201
+ "#{' '*indent}<#{opts[:cell_type]}#{options_for_element(el)}>#{res.empty? ? "&nbsp;" : res}</#{opts[:cell_type]}>\n"
202
+ end
203
+
204
+ def convert_br(el, indent, opts)
205
+ "<br />"
206
+ end
207
+
208
+ def convert_a(el, indent, opts)
209
+ if el.options[:attr]['href'] =~ /^mailto:/
210
+ el = Marshal.load(Marshal.dump(el)) # so that the original is not changed
211
+ href = obfuscate(el.options[:attr]['href'].sub(/^mailto:/, ''))
212
+ mailto = obfuscate('mailto')
213
+ el.options[:attr]['href'] = "#{mailto}:#{href}"
214
+ end
215
+ res = inner(el, indent, opts)
216
+ res = obfuscate(res) if el.options[:obfuscate_text]
217
+ "<a#{options_for_element(el)}>#{res}</a>"
218
+ end
219
+
220
+ def convert_img(el, indent, opts)
221
+ "<img#{options_for_element(el)} />"
222
+ end
223
+
224
+ def convert_codespan(el, indent, opts)
225
+ "<code#{options_for_element(el)}>#{escape_html(el.value)}</code>"
226
+ end
227
+
228
+ def convert_footnote(el, indent, opts)
229
+ number = @footnote_counter
230
+ @footnote_counter += 1
231
+ @footnotes << [el.options[:name], @doc.parse_infos[:footnotes][el.options[:name]]]
232
+ "<sup id=\"fnref:#{el.options[:name]}\"><a href=\"#fn:#{el.options[:name]}\" rel=\"footnote\">#{number}</a></sup>"
233
+ end
234
+
235
+ def convert_raw(el, indent, opts)
236
+ el.value
237
+ end
238
+
239
+ def convert_em(el, indent, opts)
240
+ "<#{el.type}#{options_for_element(el)}>#{inner(el, indent, opts)}</#{el.type}>"
241
+ end
242
+ alias :convert_strong :convert_em
243
+
244
+ def convert_entity(el, indent, opts)
245
+ "&#{el.value.kind_of?(Integer) ? '#' : ''}#{el.value};"
246
+ end
247
+
248
+ TYPOGRAPHIC_SYMS = {
249
+ :mdash => '&mdash;', :ndash => '&ndash;', :ellipsis => '&hellip;',
250
+ :laquo_space => '&laquo;&nbsp;', :raquo_space => '&nbsp;&raquo;',
251
+ :laquo => '&laquo;', :raquo => '&raquo;'
252
+ }
253
+ def convert_typographic_sym(el, indent, opts)
254
+ TYPOGRAPHIC_SYMS[el.value]
255
+ end
256
+
257
+ def convert_smart_quote(el, indent, opts)
258
+ "&#{el.value};"
259
+ end
260
+
261
+ def convert_root(el, indent, opts)
262
+ inner(el, indent, opts) << footnote_content
263
+ end
264
+
265
+ # Helper method for obfuscating the +text+ by using HTML entities.
266
+ def obfuscate(text)
267
+ result = ""
268
+ text.each_byte do |b|
269
+ result += (b > 128 ? b.chr : "&#%03d;" % b)
270
+ end
271
+ result
272
+ end
273
+
274
+ # Return a HTML list with the footnote content for the used footnotes.
275
+ def footnote_content
276
+ ol = Element.new(:ol)
277
+ ol.options[:attr] = {'start' => @footnote_start} if @footnote_start != 1
278
+ @footnotes.each do |name, data|
279
+ li = Element.new(:li, nil, {:attr => {:id => "fn:#{name}"}, :first_is_block => true})
280
+ li.children = Marshal.load(Marshal.dump(data[:content].children)) #TODO: probably remove this!!!!
281
+ ol.children << li
282
+
283
+ ref = Element.new(:raw, "<a href=\"#fnref:#{name}\" rev=\"footnote\">&#8617;</a>")
284
+ if li.children.last.type == :p
285
+ para = li.children.last
286
+ else
287
+ li.children << (para = Element.new(:p))
288
+ end
289
+ para.children << ref
290
+ end
291
+ (ol.children.empty? ? '' : "<div class=\"footnotes\">\n#{convert(ol, 2)}</div>\n")
292
+ end
293
+
294
+ # Return the string with the attributes of the element +el+.
295
+ def options_for_element(el)
296
+ (el.options[:attr] || {}).map {|k,v| v.nil? ? '' : " #{k}=\"#{escape_html(v.to_s, :no_entities)}\"" }.sort.join('')
297
+ end
298
+
299
+ ESCAPE_MAP = {
300
+ '<' => '&lt;',
301
+ '>' => '&gt;',
302
+ '&' => '&amp;',
303
+ '"' => '&quot;'
304
+ }
305
+ ESCAPE_ALL_RE = Regexp.union(*ESCAPE_MAP.collect {|k,v| k})
306
+ ESCAPE_NO_ENTITIES_RE = Regexp.union(REXML::Parsers::BaseParser::REFERENCE_RE, ESCAPE_ALL_RE)
307
+ ESCAPE_NORMAL = Regexp.union(REXML::Parsers::BaseParser::REFERENCE_RE, /<|>|&/)
308
+ ESCAPE_RE_FROM_TYPE = {
309
+ :all => ESCAPE_ALL_RE,
310
+ :no_entities => ESCAPE_NO_ENTITIES_RE,
311
+ :text => ESCAPE_NORMAL
312
+ }
313
+
314
+ # Escape the special HTML characters in the string +str+. The parameter +type+ specifies what
315
+ # is escaped: <tt>:all</tt> - all special HTML characters as well as entities,
316
+ # <tt>:no_entities</tt> - all special HTML characters but no entities, <tt>:text</tt> - all
317
+ # special HTML characters except the quotation mark but no entities.
318
+ def escape_html(str, type = :all)
319
+ str.gsub(ESCAPE_RE_FROM_TYPE[type]) {|m| ESCAPE_MAP[m] || m}
320
+ end
321
+
322
+ end
323
+
324
+ end
325
+ end