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.
- data/ChangeLog +276 -0
- data/Rakefile +2 -2
- data/VERSION +1 -1
- data/benchmark/benchmark.rb +1 -1
- data/benchmark/historic-jruby-1.4.0.dat +7 -7
- data/benchmark/historic-ruby-1.8.6.dat +7 -7
- data/benchmark/historic-ruby-1.8.7.dat +7 -7
- data/benchmark/historic-ruby-1.9.1p243.dat +7 -7
- data/benchmark/historic-ruby-1.9.2dev.dat +7 -7
- data/bin/kramdown +46 -1
- data/doc/index.page +2 -2
- data/doc/syntax.page +7 -3
- data/lib/kramdown/converter.rb +6 -285
- data/lib/kramdown/converter/base.rb +75 -0
- data/lib/kramdown/converter/html.rb +325 -0
- data/lib/kramdown/converter/latex.rb +516 -0
- data/lib/kramdown/document.rb +36 -66
- data/lib/kramdown/options.rb +262 -0
- data/lib/kramdown/parser/kramdown.rb +36 -17
- data/lib/kramdown/parser/kramdown/attribute_list.rb +1 -1
- data/lib/kramdown/parser/kramdown/autolink.rb +1 -1
- data/lib/kramdown/parser/kramdown/codespan.rb +1 -1
- data/lib/kramdown/parser/kramdown/emphasis.rb +2 -2
- data/lib/kramdown/parser/kramdown/escaped_chars.rb +2 -2
- data/lib/kramdown/parser/kramdown/extension.rb +46 -2
- data/lib/kramdown/parser/kramdown/footnote.rb +1 -1
- data/lib/kramdown/parser/kramdown/html.rb +2 -2
- data/lib/kramdown/parser/kramdown/html_entity.rb +4 -5
- data/lib/kramdown/parser/kramdown/line_break.rb +1 -1
- data/lib/kramdown/parser/kramdown/link.rb +2 -2
- data/lib/kramdown/parser/kramdown/smart_quotes.rb +213 -0
- data/lib/kramdown/parser/kramdown/typographic_symbol.rb +1 -1
- data/lib/kramdown/version.rb +1 -1
- data/test/testcases/encoding.html +46 -0
- data/test/testcases/encoding.text +28 -0
- data/test/testcases/span/01_link/inline.html +1 -1
- data/test/testcases/span/01_link/reference.html +2 -2
- data/test/testcases/span/escaped_chars/normal.html +4 -0
- data/test/testcases/span/escaped_chars/normal.text +4 -0
- data/test/testcases/span/text_substitutions/entities.html +1 -1
- data/test/testcases/span/text_substitutions/typography.html +12 -0
- data/test/testcases/span/text_substitutions/typography.text +12 -0
- metadata +9 -3
- 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
|
data/lib/kramdown/converter.rb
CHANGED
@@ -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
|
-
|
32
|
-
|
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}\">⋅</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? ? " " : 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 => '—', :ndash => '–', :ellipsis => '…',
|
251
|
-
:laquo_space => '« ', :raquo_space => ' »',
|
252
|
-
:laquo => '«', :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\">↩</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
|
-
'<' => '<',
|
298
|
-
'>' => '>',
|
299
|
-
'"' => '"',
|
300
|
-
'&' => '&'
|
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}\">⋅</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? ? " " : 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 => '—', :ndash => '–', :ellipsis => '…',
|
250
|
+
:laquo_space => '« ', :raquo_space => ' »',
|
251
|
+
:laquo => '«', :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\">↩</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
|
+
'<' => '<',
|
301
|
+
'>' => '>',
|
302
|
+
'&' => '&',
|
303
|
+
'"' => '"'
|
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
|