bean-kramdown 0.13.5
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.
- data/AUTHORS +1 -0
- data/CONTRIBUTERS +11 -0
- data/COPYING +24 -0
- data/ChangeLog +6683 -0
- data/GPL +674 -0
- data/README +43 -0
- data/VERSION +1 -0
- data/bin/kramdown +78 -0
- data/lib/kramdown.rb +23 -0
- data/lib/kramdown/compatibility.rb +49 -0
- data/lib/kramdown/converter.rb +41 -0
- data/lib/kramdown/converter/base.rb +169 -0
- data/lib/kramdown/converter/bean_html.rb +71 -0
- data/lib/kramdown/converter/html.rb +411 -0
- data/lib/kramdown/converter/kramdown.rb +428 -0
- data/lib/kramdown/converter/latex.rb +607 -0
- data/lib/kramdown/converter/toc.rb +82 -0
- data/lib/kramdown/document.rb +119 -0
- data/lib/kramdown/element.rb +524 -0
- data/lib/kramdown/error.rb +30 -0
- data/lib/kramdown/options.rb +373 -0
- data/lib/kramdown/parser.rb +39 -0
- data/lib/kramdown/parser/base.rb +136 -0
- data/lib/kramdown/parser/bean_kramdown.rb +25 -0
- data/lib/kramdown/parser/bean_kramdown/info_box.rb +52 -0
- data/lib/kramdown/parser/bean_kramdown/oembed.rb +230 -0
- data/lib/kramdown/parser/html.rb +570 -0
- data/lib/kramdown/parser/kramdown.rb +339 -0
- data/lib/kramdown/parser/kramdown/abbreviation.rb +71 -0
- data/lib/kramdown/parser/kramdown/autolink.rb +53 -0
- data/lib/kramdown/parser/kramdown/blank_line.rb +43 -0
- data/lib/kramdown/parser/kramdown/block_boundary.rb +46 -0
- data/lib/kramdown/parser/kramdown/blockquote.rb +51 -0
- data/lib/kramdown/parser/kramdown/codeblock.rb +63 -0
- data/lib/kramdown/parser/kramdown/codespan.rb +56 -0
- data/lib/kramdown/parser/kramdown/emphasis.rb +70 -0
- data/lib/kramdown/parser/kramdown/eob.rb +39 -0
- data/lib/kramdown/parser/kramdown/escaped_chars.rb +38 -0
- data/lib/kramdown/parser/kramdown/extensions.rb +204 -0
- data/lib/kramdown/parser/kramdown/footnote.rb +74 -0
- data/lib/kramdown/parser/kramdown/header.rb +68 -0
- data/lib/kramdown/parser/kramdown/horizontal_rule.rb +39 -0
- data/lib/kramdown/parser/kramdown/html.rb +169 -0
- data/lib/kramdown/parser/kramdown/html_entity.rb +44 -0
- data/lib/kramdown/parser/kramdown/image.rb +157 -0
- data/lib/kramdown/parser/kramdown/line_break.rb +38 -0
- data/lib/kramdown/parser/kramdown/link.rb +154 -0
- data/lib/kramdown/parser/kramdown/list.rb +240 -0
- data/lib/kramdown/parser/kramdown/math.rb +65 -0
- data/lib/kramdown/parser/kramdown/paragraph.rb +63 -0
- data/lib/kramdown/parser/kramdown/smart_quotes.rb +214 -0
- data/lib/kramdown/parser/kramdown/table.rb +178 -0
- data/lib/kramdown/parser/kramdown/typographic_symbol.rb +52 -0
- data/lib/kramdown/parser/markdown.rb +69 -0
- data/lib/kramdown/utils.rb +42 -0
- data/lib/kramdown/utils/entities.rb +348 -0
- data/lib/kramdown/utils/html.rb +85 -0
- data/lib/kramdown/utils/ordered_hash.rb +100 -0
- data/lib/kramdown/version.rb +28 -0
- metadata +140 -0
@@ -0,0 +1,411 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
#
|
3
|
+
#--
|
4
|
+
# Copyright (C) 2009-2012 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
|
+
#
|
31
|
+
# You can customize the HTML converter by sub-classing it and overriding the +convert_NAME+
|
32
|
+
# methods. Each such method takes the following parameters:
|
33
|
+
#
|
34
|
+
# [+el+] The element of type +NAME+ to be converted.
|
35
|
+
#
|
36
|
+
# [+indent+] A number representing the current amount of spaces for indent (only used for
|
37
|
+
# block-level elements).
|
38
|
+
#
|
39
|
+
# The return value of such a method has to be a string containing the element +el+ formatted as
|
40
|
+
# HTML element.
|
41
|
+
class Html < Base
|
42
|
+
|
43
|
+
begin
|
44
|
+
require 'coderay'
|
45
|
+
|
46
|
+
# Highlighting via coderay is available if this constant is +true+.
|
47
|
+
HIGHLIGHTING_AVAILABLE = true
|
48
|
+
rescue LoadError
|
49
|
+
HIGHLIGHTING_AVAILABLE = false # :nodoc:
|
50
|
+
end
|
51
|
+
|
52
|
+
include ::Kramdown::Utils::Html
|
53
|
+
|
54
|
+
|
55
|
+
# The amount of indentation used when nesting HTML tags.
|
56
|
+
attr_accessor :indent
|
57
|
+
|
58
|
+
# Initialize the HTML converter with the given Kramdown document +doc+.
|
59
|
+
def initialize(root, options)
|
60
|
+
super
|
61
|
+
@footnote_counter = @footnote_start = @options[:footnote_nr]
|
62
|
+
@footnotes = []
|
63
|
+
@toc = []
|
64
|
+
@toc_code = nil
|
65
|
+
@indent = 2
|
66
|
+
@stack = []
|
67
|
+
end
|
68
|
+
|
69
|
+
# The mapping of element type to conversion method.
|
70
|
+
DISPATCHER = Hash.new {|h,k| h[k] = "convert_#{k}"}
|
71
|
+
|
72
|
+
# Dispatch the conversion of the element +el+ to a +convert_TYPE+ method using the +type+ of
|
73
|
+
# the element.
|
74
|
+
def convert(el, indent = -@indent)
|
75
|
+
send(DISPATCHER[el.type], el, indent)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Return the converted content of the children of +el+ as a string. The parameter +indent+ has
|
79
|
+
# to be the amount of indentation used for the element +el+.
|
80
|
+
#
|
81
|
+
# Pushes +el+ onto the @stack before converting the child elements and pops it from the stack
|
82
|
+
# afterwards.
|
83
|
+
def inner(el, indent)
|
84
|
+
result = ''
|
85
|
+
indent += @indent
|
86
|
+
@stack.push(el)
|
87
|
+
el.children.each do |inner_el|
|
88
|
+
result << send(DISPATCHER[inner_el.type], inner_el, indent)
|
89
|
+
end
|
90
|
+
@stack.pop
|
91
|
+
result
|
92
|
+
end
|
93
|
+
|
94
|
+
def convert_blank(el, indent)
|
95
|
+
"\n"
|
96
|
+
end
|
97
|
+
|
98
|
+
def convert_text(el, indent)
|
99
|
+
escape_html(el.value, :text)
|
100
|
+
end
|
101
|
+
|
102
|
+
def convert_p(el, indent)
|
103
|
+
if el.options[:transparent]
|
104
|
+
inner(el, indent)
|
105
|
+
else
|
106
|
+
"#{' '*indent}<p#{html_attributes(el.attr)}>#{inner(el, indent)}</p>\n"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def convert_codeblock(el, indent)
|
111
|
+
if el.attr['lang'] && HIGHLIGHTING_AVAILABLE
|
112
|
+
attr = el.attr.dup
|
113
|
+
opts = {:wrap => @options[:coderay_wrap], :line_numbers => @options[:coderay_line_numbers],
|
114
|
+
:line_number_start => @options[:coderay_line_number_start], :tab_width => @options[:coderay_tab_width],
|
115
|
+
:bold_every => @options[:coderay_bold_every], :css => @options[:coderay_css]}
|
116
|
+
result = CodeRay.scan(el.value, attr.delete('lang').to_sym).html(opts).chomp << "\n"
|
117
|
+
"#{' '*indent}<div#{html_attributes(attr)}>#{result}#{' '*indent}</div>\n"
|
118
|
+
else
|
119
|
+
result = escape_html(el.value)
|
120
|
+
result.chomp!
|
121
|
+
if el.attr['class'].to_s =~ /\bshow-whitespaces\b/
|
122
|
+
result.gsub!(/(?:(^[ \t]+)|([ \t]+$)|([ \t]+))/) do |m|
|
123
|
+
suffix = ($1 ? '-l' : ($2 ? '-r' : ''))
|
124
|
+
m.scan(/./).map do |c|
|
125
|
+
case c
|
126
|
+
when "\t" then "<span class=\"ws-tab#{suffix}\">\t</span>"
|
127
|
+
when " " then "<span class=\"ws-space#{suffix}\">⋅</span>"
|
128
|
+
end
|
129
|
+
end.join('')
|
130
|
+
end
|
131
|
+
end
|
132
|
+
"#{' '*indent}<pre#{html_attributes(el.attr)}><code>#{result}\n</code></pre>\n"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def convert_blockquote(el, indent)
|
137
|
+
"#{' '*indent}<blockquote#{html_attributes(el.attr)}>\n#{inner(el, indent)}#{' '*indent}</blockquote>\n"
|
138
|
+
end
|
139
|
+
|
140
|
+
def convert_header(el, indent)
|
141
|
+
attr = el.attr.dup
|
142
|
+
if @options[:auto_ids] && !attr['id']
|
143
|
+
attr['id'] = generate_id(el.options[:raw_text])
|
144
|
+
end
|
145
|
+
@toc << [el.options[:level], attr['id'], el.children] if attr['id'] && in_toc?(el)
|
146
|
+
"#{' '*indent}<h#{el.options[:level]}#{html_attributes(attr)}>#{inner(el, indent)}</h#{el.options[:level]}>\n"
|
147
|
+
end
|
148
|
+
|
149
|
+
def convert_hr(el, indent)
|
150
|
+
"#{' '*indent}<hr />\n"
|
151
|
+
end
|
152
|
+
|
153
|
+
def convert_ul(el, indent)
|
154
|
+
if !@toc_code && (el.options[:ial][:refs].include?('toc') rescue nil) && (el.type == :ul || el.type == :ol)
|
155
|
+
@toc_code = [el.type, el.attr, (0..128).to_a.map{|a| rand(36).to_s(36)}.join]
|
156
|
+
@toc_code.last
|
157
|
+
else
|
158
|
+
"#{' '*indent}<#{el.type}#{html_attributes(el.attr)}>\n#{inner(el, indent)}#{' '*indent}</#{el.type}>\n"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
alias :convert_ol :convert_ul
|
162
|
+
alias :convert_dl :convert_ul
|
163
|
+
|
164
|
+
def convert_li(el, indent)
|
165
|
+
output = ' '*indent << "<#{el.type}" << html_attributes(el.attr) << ">"
|
166
|
+
res = inner(el, indent)
|
167
|
+
if el.children.empty? || (el.children.first.type == :p && el.children.first.options[:transparent])
|
168
|
+
output << res << (res =~ /\n\Z/ ? ' '*indent : '')
|
169
|
+
else
|
170
|
+
output << "\n" << res << ' '*indent
|
171
|
+
end
|
172
|
+
output << "</#{el.type}>\n"
|
173
|
+
end
|
174
|
+
alias :convert_dd :convert_li
|
175
|
+
|
176
|
+
def convert_dt(el, indent)
|
177
|
+
"#{' '*indent}<dt#{html_attributes(el.attr)}>#{inner(el, indent)}</dt>\n"
|
178
|
+
end
|
179
|
+
|
180
|
+
# A list of all HTML tags that need to have a body (even if the body is empty).
|
181
|
+
HTML_TAGS_WITH_BODY=['div', 'span', 'script', 'iframe', 'textarea', 'a'] # :nodoc:
|
182
|
+
|
183
|
+
def convert_html_element(el, indent)
|
184
|
+
res = inner(el, indent)
|
185
|
+
if el.options[:category] == :span
|
186
|
+
"<#{el.value}#{html_attributes(el.attr)}" << (!res.empty? || HTML_TAGS_WITH_BODY.include?(el.value) ? ">#{res}</#{el.value}>" : " />")
|
187
|
+
else
|
188
|
+
output = ''
|
189
|
+
output << ' '*indent if @stack.last.type != :html_element || @stack.last.options[:content_model] != :raw
|
190
|
+
output << "<#{el.value}#{html_attributes(el.attr)}"
|
191
|
+
if !res.empty? && el.options[:content_model] != :block
|
192
|
+
output << ">#{res}</#{el.value}>"
|
193
|
+
elsif !res.empty?
|
194
|
+
output << ">\n#{res.chomp}\n" << ' '*indent << "</#{el.value}>"
|
195
|
+
elsif HTML_TAGS_WITH_BODY.include?(el.value)
|
196
|
+
output << "></#{el.value}>"
|
197
|
+
else
|
198
|
+
output << " />"
|
199
|
+
end
|
200
|
+
output << "\n" if @stack.last.type != :html_element || @stack.last.options[:content_model] != :raw
|
201
|
+
output
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def convert_xml_comment(el, indent)
|
206
|
+
if el.options[:category] == :block && (@stack.last.type != :html_element || @stack.last.options[:content_model] != :raw)
|
207
|
+
' '*indent << el.value << "\n"
|
208
|
+
else
|
209
|
+
el.value
|
210
|
+
end
|
211
|
+
end
|
212
|
+
alias :convert_xml_pi :convert_xml_comment
|
213
|
+
|
214
|
+
def convert_table(el, indent)
|
215
|
+
"#{' '*indent}<table#{html_attributes(el.attr)}>\n#{inner(el, indent)}#{' '*indent}</table>\n"
|
216
|
+
end
|
217
|
+
|
218
|
+
def convert_thead(el, indent)
|
219
|
+
"#{' '*indent}<#{el.type}#{html_attributes(el.attr)}>\n#{inner(el, indent)}#{' '*indent}</#{el.type}>\n"
|
220
|
+
end
|
221
|
+
alias :convert_tbody :convert_thead
|
222
|
+
alias :convert_tfoot :convert_thead
|
223
|
+
alias :convert_tr :convert_thead
|
224
|
+
|
225
|
+
ENTITY_NBSP = ::Kramdown::Utils::Entities.entity('nbsp') # :nodoc:
|
226
|
+
|
227
|
+
def convert_td(el, indent)
|
228
|
+
res = inner(el, indent)
|
229
|
+
type = (@stack[-2].type == :thead ? :th : :td)
|
230
|
+
attr = el.attr
|
231
|
+
alignment = @stack[-3].options[:alignment][@stack.last.children.index(el)]
|
232
|
+
if alignment != :default
|
233
|
+
attr = el.attr.dup
|
234
|
+
attr['style'] = (attr.has_key?('style') ? "#{attr['style']}; ": '') << "text-align: #{alignment}"
|
235
|
+
end
|
236
|
+
"#{' '*indent}<#{type}#{html_attributes(attr)}>#{res.empty? ? entity_to_str(ENTITY_NBSP) : res}</#{type}>\n"
|
237
|
+
end
|
238
|
+
|
239
|
+
def convert_comment(el, indent)
|
240
|
+
if el.options[:category] == :block
|
241
|
+
"#{' '*indent}<!-- #{el.value} -->\n"
|
242
|
+
else
|
243
|
+
"<!-- #{el.value} -->"
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def convert_br(el, indent)
|
248
|
+
"<br />"
|
249
|
+
end
|
250
|
+
|
251
|
+
def convert_a(el, indent)
|
252
|
+
res = inner(el, indent)
|
253
|
+
attr = el.attr.dup
|
254
|
+
if attr['href'] =~ /^mailto:/
|
255
|
+
attr['href'] = obfuscate('mailto') << ":" << obfuscate(attr['href'].sub(/^mailto:/, ''))
|
256
|
+
res = obfuscate(res)
|
257
|
+
end
|
258
|
+
"<a#{html_attributes(attr)}>#{res}</a>"
|
259
|
+
end
|
260
|
+
|
261
|
+
def convert_img(el, indent)
|
262
|
+
"<img#{html_attributes(el.attr)} />"
|
263
|
+
end
|
264
|
+
|
265
|
+
def convert_codespan(el, indent)
|
266
|
+
if el.attr['lang'] && HIGHLIGHTING_AVAILABLE
|
267
|
+
attr = el.attr.dup
|
268
|
+
result = CodeRay.scan(el.value, attr.delete('lang').to_sym).html(:wrap => :span, :css => @options[:coderay_css]).chomp
|
269
|
+
"<code#{html_attributes(attr)}>#{result}</code>"
|
270
|
+
else
|
271
|
+
"<code#{html_attributes(el.attr)}>#{escape_html(el.value)}</code>"
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
def convert_footnote(el, indent)
|
276
|
+
number = @footnote_counter
|
277
|
+
@footnote_counter += 1
|
278
|
+
@footnotes << [el.options[:name], el.value]
|
279
|
+
"<sup id=\"fnref:#{el.options[:name]}\"><a href=\"#fn:#{el.options[:name]}\" rel=\"footnote\">#{number}</a></sup>"
|
280
|
+
end
|
281
|
+
|
282
|
+
def convert_raw(el, indent)
|
283
|
+
if !el.options[:type] || el.options[:type].empty? || el.options[:type].include?('html')
|
284
|
+
el.value + (el.options[:category] == :block ? "\n" : '')
|
285
|
+
else
|
286
|
+
''
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def convert_em(el, indent)
|
291
|
+
"<#{el.type}#{html_attributes(el.attr)}>#{inner(el, indent)}</#{el.type}>"
|
292
|
+
end
|
293
|
+
alias :convert_strong :convert_em
|
294
|
+
|
295
|
+
def convert_entity(el, indent)
|
296
|
+
entity_to_str(el.value, el.options[:original])
|
297
|
+
end
|
298
|
+
|
299
|
+
TYPOGRAPHIC_SYMS = {
|
300
|
+
:mdash => [::Kramdown::Utils::Entities.entity('mdash')],
|
301
|
+
:ndash => [::Kramdown::Utils::Entities.entity('ndash')],
|
302
|
+
:hellip => [::Kramdown::Utils::Entities.entity('hellip')],
|
303
|
+
:laquo_space => [::Kramdown::Utils::Entities.entity('laquo'), ::Kramdown::Utils::Entities.entity('nbsp')],
|
304
|
+
:raquo_space => [::Kramdown::Utils::Entities.entity('nbsp'), ::Kramdown::Utils::Entities.entity('raquo')],
|
305
|
+
:laquo => [::Kramdown::Utils::Entities.entity('laquo')],
|
306
|
+
:raquo => [::Kramdown::Utils::Entities.entity('raquo')]
|
307
|
+
} # :nodoc:
|
308
|
+
def convert_typographic_sym(el, indent)
|
309
|
+
TYPOGRAPHIC_SYMS[el.value].map {|e| entity_to_str(e)}.join('')
|
310
|
+
end
|
311
|
+
|
312
|
+
def convert_smart_quote(el, indent)
|
313
|
+
entity_to_str(smart_quote_entity(el))
|
314
|
+
end
|
315
|
+
|
316
|
+
def convert_math(el, indent)
|
317
|
+
block = (el.options[:category] == :block)
|
318
|
+
value = (el.value =~ /<|&/ ? "<![CDATA[#{el.value}]]>" : el.value)
|
319
|
+
"<script type=\"math/tex#{block ? '; mode=display' : ''}\">#{value}</script>#{block ? "\n" : ''}"
|
320
|
+
end
|
321
|
+
|
322
|
+
def convert_abbreviation(el, indent)
|
323
|
+
title = @root.options[:abbrev_defs][el.value]
|
324
|
+
"<abbr#{!title.empty? ? " title=\"#{title}\"" : ''}>#{el.value}</abbr>"
|
325
|
+
end
|
326
|
+
|
327
|
+
def convert_root(el, indent)
|
328
|
+
result = inner(el, indent)
|
329
|
+
result << footnote_content
|
330
|
+
if @toc_code
|
331
|
+
toc_tree = generate_toc_tree(@toc, @toc_code[0], @toc_code[1] || {})
|
332
|
+
text = if toc_tree.children.size > 0
|
333
|
+
convert(toc_tree, 0)
|
334
|
+
else
|
335
|
+
''
|
336
|
+
end
|
337
|
+
result.sub!(/#{@toc_code.last}/, text)
|
338
|
+
end
|
339
|
+
result
|
340
|
+
end
|
341
|
+
|
342
|
+
# Generate and return an element tree for the table of contents.
|
343
|
+
def generate_toc_tree(toc, type, attr)
|
344
|
+
sections = Element.new(type, nil, attr)
|
345
|
+
sections.attr['id'] ||= 'markdown-toc'
|
346
|
+
stack = []
|
347
|
+
toc.each do |level, id, children|
|
348
|
+
li = Element.new(:li, nil, nil, {:level => level})
|
349
|
+
li.children << Element.new(:p, nil, nil, {:transparent => true})
|
350
|
+
a = Element.new(:a, nil, {'href' => "##{id}"})
|
351
|
+
a.children.concat(children)
|
352
|
+
li.children.last.children << a
|
353
|
+
li.children << Element.new(type)
|
354
|
+
|
355
|
+
success = false
|
356
|
+
while !success
|
357
|
+
if stack.empty?
|
358
|
+
sections.children << li
|
359
|
+
stack << li
|
360
|
+
success = true
|
361
|
+
elsif stack.last.options[:level] < li.options[:level]
|
362
|
+
stack.last.children.last.children << li
|
363
|
+
stack << li
|
364
|
+
success = true
|
365
|
+
else
|
366
|
+
item = stack.pop
|
367
|
+
item.children.pop unless item.children.last.children.size > 0
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
while !stack.empty?
|
372
|
+
item = stack.pop
|
373
|
+
item.children.pop unless item.children.last.children.size > 0
|
374
|
+
end
|
375
|
+
sections
|
376
|
+
end
|
377
|
+
|
378
|
+
# Obfuscate the +text+ by using HTML entities.
|
379
|
+
def obfuscate(text)
|
380
|
+
result = ""
|
381
|
+
text.each_byte do |b|
|
382
|
+
result << (b > 128 ? b.chr : "&#%03d;" % b)
|
383
|
+
end
|
384
|
+
result.force_encoding(text.encoding) if RUBY_VERSION >= '1.9'
|
385
|
+
result
|
386
|
+
end
|
387
|
+
|
388
|
+
# Return a HTML ordered list with the footnote content for the used footnotes.
|
389
|
+
def footnote_content
|
390
|
+
ol = Element.new(:ol)
|
391
|
+
ol.attr['start'] = @footnote_start if @footnote_start != 1
|
392
|
+
@footnotes.each do |name, data|
|
393
|
+
li = Element.new(:li, nil, {'id' => "fn:#{name}"})
|
394
|
+
li.children = Marshal.load(Marshal.dump(data.children))
|
395
|
+
ol.children << li
|
396
|
+
|
397
|
+
ref = Element.new(:raw, "<a href=\"#fnref:#{name}\" rel=\"reference\">↩</a>")
|
398
|
+
if li.children.last.type == :p
|
399
|
+
para = li.children.last
|
400
|
+
else
|
401
|
+
li.children << (para = Element.new(:p))
|
402
|
+
end
|
403
|
+
para.children << ref
|
404
|
+
end
|
405
|
+
(ol.children.empty? ? '' : "<div class=\"footnotes\">\n#{convert(ol, 2)}</div>\n")
|
406
|
+
end
|
407
|
+
|
408
|
+
end
|
409
|
+
|
410
|
+
end
|
411
|
+
end
|
@@ -0,0 +1,428 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
#
|
3
|
+
#--
|
4
|
+
# Copyright (C) 2009-2012 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 an element tree to the kramdown format.
|
30
|
+
class Kramdown < Base
|
31
|
+
|
32
|
+
# :stopdoc:
|
33
|
+
|
34
|
+
include ::Kramdown::Utils::Html
|
35
|
+
|
36
|
+
def initialize(root, options)
|
37
|
+
super
|
38
|
+
@linkrefs = []
|
39
|
+
@footnotes = []
|
40
|
+
@abbrevs = []
|
41
|
+
@stack = []
|
42
|
+
end
|
43
|
+
|
44
|
+
def convert(el, opts = {:indent => 0})
|
45
|
+
res = send("convert_#{el.type}", el, opts)
|
46
|
+
if ![:html_element, :li, :dd, :td].include?(el.type) && (ial = ial_for_element(el))
|
47
|
+
res << ial
|
48
|
+
res << "\n\n" if Element.category(el) == :block
|
49
|
+
elsif [:ul, :dl, :ol, :codeblock].include?(el.type) && opts[:next] &&
|
50
|
+
([el.type, :codeblock].include?(opts[:next].type) ||
|
51
|
+
(opts[:next].type == :blank && opts[:nnext] && [el.type, :codeblock].include?(opts[:nnext].type)))
|
52
|
+
res << "^\n\n"
|
53
|
+
elsif Element.category(el) == :block &&
|
54
|
+
![:li, :dd, :dt, :td, :th, :tr, :thead, :tbody, :tfoot, :blank].include?(el.type) &&
|
55
|
+
(el.type != :html_element || @stack.last.type != :html_element) &&
|
56
|
+
(el.type != :p || !el.options[:transparent])
|
57
|
+
res << "\n"
|
58
|
+
end
|
59
|
+
res
|
60
|
+
end
|
61
|
+
|
62
|
+
def inner(el, opts = {:indent => 0})
|
63
|
+
@stack.push(el)
|
64
|
+
result = ''
|
65
|
+
el.children.each_with_index do |inner_el, index|
|
66
|
+
options = opts.dup
|
67
|
+
options[:index] = index
|
68
|
+
options[:prev] = (index == 0 ? nil : el.children[index-1])
|
69
|
+
options[:pprev] = (index <= 1 ? nil : el.children[index-2])
|
70
|
+
options[:next] = (index == el.children.length - 1 ? nil : el.children[index+1])
|
71
|
+
options[:nnext] = (index >= el.children.length - 2 ? nil : el.children[index+2])
|
72
|
+
result << convert(inner_el, options)
|
73
|
+
end
|
74
|
+
@stack.pop
|
75
|
+
result
|
76
|
+
end
|
77
|
+
|
78
|
+
def convert_blank(el, opts)
|
79
|
+
""
|
80
|
+
end
|
81
|
+
|
82
|
+
ESCAPED_CHAR_RE = /(\$\$|[\\*_`\[\]\{"'|])|^[ ]{0,3}(:)/
|
83
|
+
|
84
|
+
def convert_text(el, opts)
|
85
|
+
if opts[:raw_text]
|
86
|
+
el.value
|
87
|
+
else
|
88
|
+
el.value.gsub(/\A\n/) do
|
89
|
+
opts[:prev] && opts[:prev].type == :br ? '' : "\n"
|
90
|
+
end.gsub(/\s+/, ' ').gsub(ESCAPED_CHAR_RE) { "\\#{$1 || $2}" }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def convert_p(el, opts)
|
95
|
+
w = @options[:line_width] - opts[:indent].to_s.to_i
|
96
|
+
first, second, *rest = inner(el, opts).strip.gsub(/(.{1,#{w}})( +|$\n?)/, "\\1\n").split(/\n/)
|
97
|
+
first.gsub!(/^(?:(#|>)|(\d+)\.|([+-]\s))/) { $1 || $3 ? "\\#{$1 || $3}" : "#{$2}\\."} if first
|
98
|
+
second.gsub!(/^([=-]+\s*?)$/, "\\\1") if second
|
99
|
+
res = [first, second, *rest].compact.join("\n") + "\n"
|
100
|
+
if el.children.length == 1 && el.children.first.type == :math
|
101
|
+
res = "\\#{res}"
|
102
|
+
elsif res.start_with?('\$$') && res.end_with?("\\$$\n")
|
103
|
+
res.sub!(/^\\\$\$/, '\$\$')
|
104
|
+
end
|
105
|
+
res
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
def convert_codeblock(el, opts)
|
110
|
+
el.value.split(/\n/).map {|l| l.empty? ? " " : " #{l}"}.join("\n") + "\n"
|
111
|
+
end
|
112
|
+
|
113
|
+
def convert_blockquote(el, opts)
|
114
|
+
opts[:indent] += 2
|
115
|
+
inner(el, opts).chomp.split(/\n/).map {|l| "> #{l}"}.join("\n") << "\n"
|
116
|
+
end
|
117
|
+
|
118
|
+
def convert_header(el, opts)
|
119
|
+
res = ''
|
120
|
+
res << "#{'#' * el.options[:level]} #{inner(el, opts)}"
|
121
|
+
res << " {##{el.attr['id']}}" if el.attr['id'] && !el.attr['id'].strip.empty?
|
122
|
+
res << "\n"
|
123
|
+
end
|
124
|
+
|
125
|
+
def convert_hr(el, opts)
|
126
|
+
"* * *\n"
|
127
|
+
end
|
128
|
+
|
129
|
+
def convert_ul(el, opts)
|
130
|
+
inner(el, opts).sub(/\n+\Z/, "\n")
|
131
|
+
end
|
132
|
+
alias :convert_ol :convert_ul
|
133
|
+
alias :convert_dl :convert_ul
|
134
|
+
|
135
|
+
def convert_li(el, opts)
|
136
|
+
sym, width = if @stack.last.type == :ul
|
137
|
+
['* ', el.children.first.type == :codeblock ? 4 : 2]
|
138
|
+
else
|
139
|
+
["#{opts[:index] + 1}.".ljust(4), 4]
|
140
|
+
end
|
141
|
+
if ial = ial_for_element(el)
|
142
|
+
sym << ial << " "
|
143
|
+
end
|
144
|
+
|
145
|
+
opts[:indent] += width
|
146
|
+
text = inner(el, opts)
|
147
|
+
newlines = text.scan(/\n*\Z/).first
|
148
|
+
first, *last = text.split(/\n/)
|
149
|
+
last = last.map {|l| " "*width + l}.join("\n")
|
150
|
+
text = first + (last.empty? ? "" : "\n") + last + newlines
|
151
|
+
if el.children.first.type == :p && !el.children.first.options[:transparent]
|
152
|
+
res = "#{sym}#{text}"
|
153
|
+
res << "^\n" if el.children.size == 1 && @stack.last.children.last == el &&
|
154
|
+
(@stack.last.children.any? {|c| c.children.first.type != :p} || @stack.last.children.size == 1)
|
155
|
+
res
|
156
|
+
elsif el.children.first.type == :codeblock
|
157
|
+
"#{sym}\n #{text}"
|
158
|
+
else
|
159
|
+
"#{sym}#{text}"
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def convert_dd(el, opts)
|
164
|
+
sym, width = ": ", (el.children.first.type == :codeblock ? 4 : 2)
|
165
|
+
if ial = ial_for_element(el)
|
166
|
+
sym << ial << " "
|
167
|
+
end
|
168
|
+
|
169
|
+
opts[:indent] += width
|
170
|
+
text = inner(el, opts)
|
171
|
+
newlines = text.scan(/\n*\Z/).first
|
172
|
+
first, *last = text.split(/\n/)
|
173
|
+
last = last.map {|l| " "*width + l}.join("\n")
|
174
|
+
text = first + (last.empty? ? "" : "\n") + last + newlines
|
175
|
+
text.chomp! if text =~ /\n\n\Z/ && opts[:next] && opts[:next].type == :dd
|
176
|
+
text << "\n" if text !~ /\n\n\Z/ && opts[:next] && opts[:next].type == :dt
|
177
|
+
if el.children.first.type == :p && !el.children.first.options[:transparent]
|
178
|
+
"\n#{sym}#{text}"
|
179
|
+
elsif el.children.first.type == :codeblock
|
180
|
+
"#{sym}\n #{text}"
|
181
|
+
else
|
182
|
+
"#{sym}#{text}"
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def convert_dt(el, opts)
|
187
|
+
inner(el, opts) << "\n"
|
188
|
+
end
|
189
|
+
|
190
|
+
HTML_TAGS_WITH_BODY=['div', 'script', 'iframe', 'textarea']
|
191
|
+
|
192
|
+
def convert_html_element(el, opts)
|
193
|
+
markdown_attr = el.options[:category] == :block && el.children.any? do |c|
|
194
|
+
c.type != :html_element && (c.type != :p || !c.options[:transparent]) && Element.category(c) == :block
|
195
|
+
end
|
196
|
+
opts[:force_raw_text] = true if %w{script pre code}.include?(el.value)
|
197
|
+
opts[:raw_text] = opts[:force_raw_text] || opts[:block_raw_text] || (el.options[:category] != :span && !markdown_attr)
|
198
|
+
opts[:block_raw_text] = true if el.options[:category] == :block && opts[:raw_text]
|
199
|
+
res = inner(el, opts)
|
200
|
+
if el.options[:category] == :span
|
201
|
+
"<#{el.value}#{html_attributes(el.attr)}" << (!res.empty? || HTML_TAGS_WITH_BODY.include?(el.value) ? ">#{res}</#{el.value}>" : " />")
|
202
|
+
else
|
203
|
+
output = ''
|
204
|
+
output << "<#{el.value}#{html_attributes(el.attr)}"
|
205
|
+
output << " markdown=\"1\"" if markdown_attr
|
206
|
+
if !res.empty? && el.options[:content_model] != :block
|
207
|
+
output << ">#{res}</#{el.value}>"
|
208
|
+
elsif !res.empty?
|
209
|
+
output << ">\n#{res}" << "</#{el.value}>"
|
210
|
+
elsif HTML_TAGS_WITH_BODY.include?(el.value)
|
211
|
+
output << "></#{el.value}>"
|
212
|
+
else
|
213
|
+
output << " />"
|
214
|
+
end
|
215
|
+
output << "\n" if @stack.last.type != :html_element || @stack.last.options[:content_model] != :raw
|
216
|
+
output
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def convert_xml_comment(el, opts)
|
221
|
+
if el.options[:category] == :block && (@stack.last.type != :html_element || @stack.last.options[:content_model] != :raw)
|
222
|
+
el.value + "\n"
|
223
|
+
else
|
224
|
+
el.value.dup
|
225
|
+
end
|
226
|
+
end
|
227
|
+
alias :convert_xml_pi :convert_xml_comment
|
228
|
+
|
229
|
+
def convert_table(el, opts)
|
230
|
+
opts[:alignment] = el.options[:alignment]
|
231
|
+
inner(el, opts)
|
232
|
+
end
|
233
|
+
|
234
|
+
def convert_thead(el, opts)
|
235
|
+
rows = inner(el, opts)
|
236
|
+
if opts[:alignment].all? {|a| a == :default}
|
237
|
+
"#{rows}|" + "-"*10 + "\n"
|
238
|
+
else
|
239
|
+
"#{rows}| " + opts[:alignment].map do |a|
|
240
|
+
case a
|
241
|
+
when :left then ":-"
|
242
|
+
when :right then "-:"
|
243
|
+
when :center then ":-:"
|
244
|
+
when :default then "-"
|
245
|
+
end
|
246
|
+
end.join(' ') + "\n"
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def convert_tbody(el, opts)
|
251
|
+
res = ''
|
252
|
+
res << inner(el, opts)
|
253
|
+
res << '|' << '-'*10 << "\n" if opts[:next] && opts[:next].type == :tbody
|
254
|
+
res
|
255
|
+
end
|
256
|
+
|
257
|
+
def convert_tfoot(el, opts)
|
258
|
+
"|" + "="*10 + "\n#{inner(el, opts)}"
|
259
|
+
end
|
260
|
+
|
261
|
+
def convert_tr(el, opts)
|
262
|
+
"| " + el.children.map {|c| convert(c, opts)}.join(" | ") + " |\n"
|
263
|
+
end
|
264
|
+
|
265
|
+
def convert_td(el, opts)
|
266
|
+
inner(el, opts)
|
267
|
+
end
|
268
|
+
|
269
|
+
def convert_comment(el, opts)
|
270
|
+
if el.options[:category] == :block
|
271
|
+
"{::comment}\n#{el.value}\n{:/}\n"
|
272
|
+
else
|
273
|
+
"{::comment}#{el.value}{:/}"
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def convert_br(el, opts)
|
278
|
+
" \n"
|
279
|
+
end
|
280
|
+
|
281
|
+
def convert_a(el, opts)
|
282
|
+
if el.attr['href'].empty?
|
283
|
+
"[#{inner(el, opts)}]()"
|
284
|
+
elsif el.attr['href'] =~ /^(?:http|ftp)/ || el.attr['href'].count("()") > 0
|
285
|
+
index = if link_el = @linkrefs.find {|c| c.attr['href'] == el.attr['href']}
|
286
|
+
@linkrefs.index(link_el) + 1
|
287
|
+
else
|
288
|
+
@linkrefs << el
|
289
|
+
@linkrefs.size
|
290
|
+
end
|
291
|
+
"[#{inner(el, opts)}][#{index}]"
|
292
|
+
else
|
293
|
+
title = el.attr['title'].to_s.empty? ? '' : ' "' + el.attr['title'].gsub(/"/, """) + '"'
|
294
|
+
"[#{inner(el, opts)}](#{el.attr['href']}#{title})"
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
def convert_img(el, opts)
|
299
|
+
if el.attr['src'].empty?
|
300
|
+
"![#{el.attr['alt']}]()"
|
301
|
+
else
|
302
|
+
title = (el.attr['title'] ? ' "' + el.attr['title'].gsub(/"/, """) + '"' : '')
|
303
|
+
link = if el.attr['src'].count("()") > 0
|
304
|
+
"<#{el.attr['src']}>"
|
305
|
+
else
|
306
|
+
el.attr['src']
|
307
|
+
end
|
308
|
+
"![#{el.attr['alt']}](#{link}#{title})"
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
def convert_codespan(el, opts)
|
313
|
+
delim = (el.value.scan(/`+/).max || '') + '`'
|
314
|
+
"#{delim}#{' ' if delim.size > 1}#{el.value}#{' ' if delim.size > 1}#{delim}"
|
315
|
+
end
|
316
|
+
|
317
|
+
def convert_footnote(el, opts)
|
318
|
+
@footnotes << [el.options[:name], el.value]
|
319
|
+
"[^#{el.options[:name]}]"
|
320
|
+
end
|
321
|
+
|
322
|
+
def convert_raw(el, opts)
|
323
|
+
attr = (el.options[:type] || []).join(' ')
|
324
|
+
attr = " type=\"#{attr}\"" if attr.length > 0
|
325
|
+
if @stack.last.type == :html_element
|
326
|
+
el.value
|
327
|
+
elsif el.options[:category] == :block
|
328
|
+
"{::nomarkdown#{attr}}\n#{el.value}\n{:/}\n"
|
329
|
+
else
|
330
|
+
"{::nomarkdown#{attr}}#{el.value}{:/}"
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
def convert_em(el, opts)
|
335
|
+
"*#{inner(el, opts)}*"
|
336
|
+
end
|
337
|
+
|
338
|
+
def convert_strong(el, opts)
|
339
|
+
"**#{inner(el, opts)}**"
|
340
|
+
end
|
341
|
+
|
342
|
+
def convert_entity(el, opts)
|
343
|
+
entity_to_str(el.value, el.options[:original])
|
344
|
+
end
|
345
|
+
|
346
|
+
TYPOGRAPHIC_SYMS = {
|
347
|
+
:mdash => '---', :ndash => '--', :hellip => '...',
|
348
|
+
:laquo_space => '<< ', :raquo_space => ' >>',
|
349
|
+
:laquo => '<<', :raquo => '>>'
|
350
|
+
}
|
351
|
+
def convert_typographic_sym(el, opts)
|
352
|
+
TYPOGRAPHIC_SYMS[el.value]
|
353
|
+
end
|
354
|
+
|
355
|
+
def convert_smart_quote(el, opts)
|
356
|
+
el.value.to_s =~ /[rl]dquo/ ? "\"" : "'"
|
357
|
+
end
|
358
|
+
|
359
|
+
def convert_math(el, opts)
|
360
|
+
"$$#{el.value}$$" + (el.options[:category] == :block ? "\n" : '')
|
361
|
+
end
|
362
|
+
|
363
|
+
def convert_abbreviation(el, opts)
|
364
|
+
el.value
|
365
|
+
end
|
366
|
+
|
367
|
+
def convert_root(el, opts)
|
368
|
+
res = inner(el, opts)
|
369
|
+
res << create_link_defs
|
370
|
+
res << create_footnote_defs
|
371
|
+
res << create_abbrev_defs
|
372
|
+
res
|
373
|
+
end
|
374
|
+
|
375
|
+
def create_link_defs
|
376
|
+
res = ''
|
377
|
+
res << "\n\n" if @linkrefs.size > 0
|
378
|
+
@linkrefs.each_with_index do |el, i|
|
379
|
+
title = el.attr['title']
|
380
|
+
res << "[#{i+1}]: #{el.attr['href']} #{title ? '"' + title.gsub(/"/, """) + '"' : ''}\n"
|
381
|
+
end
|
382
|
+
res
|
383
|
+
end
|
384
|
+
|
385
|
+
def create_footnote_defs
|
386
|
+
res = ''
|
387
|
+
@footnotes.each do |name, data|
|
388
|
+
res << "[^#{name}]:\n"
|
389
|
+
res << inner(data).chomp.split(/\n/).map {|l| " #{l}"}.join("\n") + "\n\n"
|
390
|
+
end
|
391
|
+
res
|
392
|
+
end
|
393
|
+
|
394
|
+
def create_abbrev_defs
|
395
|
+
return '' unless @root.options[:abbrev_defs]
|
396
|
+
res = ''
|
397
|
+
@root.options[:abbrev_defs].each do |name, text|
|
398
|
+
res << "*[#{name}]: #{text}\n"
|
399
|
+
end
|
400
|
+
res
|
401
|
+
end
|
402
|
+
|
403
|
+
# Return the IAL containing the attributes of the element +el+.
|
404
|
+
def ial_for_element(el)
|
405
|
+
res = el.attr.map do |k,v|
|
406
|
+
next if [:img, :a].include?(el.type) && ['href', 'src', 'alt', 'title'].include?(k)
|
407
|
+
next if el.type == :header && k == 'id' && !v.strip.empty?
|
408
|
+
if v.nil?
|
409
|
+
''
|
410
|
+
elsif k == 'class' && !v.empty?
|
411
|
+
" " + v.split(/\s+/).map {|w| ".#{w}"}.join(" ")
|
412
|
+
elsif k == 'id' && !v.strip.empty?
|
413
|
+
" ##{v}"
|
414
|
+
else
|
415
|
+
" #{k}=\"#{v.to_s}\""
|
416
|
+
end
|
417
|
+
end.compact.join('')
|
418
|
+
res = "toc" + (res.strip.empty? ? '' : " #{res}") if (el.type == :ul || el.type == :ol) &&
|
419
|
+
(el.options[:ial][:refs].include?('toc') rescue nil)
|
420
|
+
res.strip.empty? ? nil : "{:#{res}}"
|
421
|
+
end
|
422
|
+
|
423
|
+
# :startdoc:
|
424
|
+
|
425
|
+
end
|
426
|
+
|
427
|
+
end
|
428
|
+
end
|