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.
Files changed (60) hide show
  1. data/AUTHORS +1 -0
  2. data/CONTRIBUTERS +11 -0
  3. data/COPYING +24 -0
  4. data/ChangeLog +6683 -0
  5. data/GPL +674 -0
  6. data/README +43 -0
  7. data/VERSION +1 -0
  8. data/bin/kramdown +78 -0
  9. data/lib/kramdown.rb +23 -0
  10. data/lib/kramdown/compatibility.rb +49 -0
  11. data/lib/kramdown/converter.rb +41 -0
  12. data/lib/kramdown/converter/base.rb +169 -0
  13. data/lib/kramdown/converter/bean_html.rb +71 -0
  14. data/lib/kramdown/converter/html.rb +411 -0
  15. data/lib/kramdown/converter/kramdown.rb +428 -0
  16. data/lib/kramdown/converter/latex.rb +607 -0
  17. data/lib/kramdown/converter/toc.rb +82 -0
  18. data/lib/kramdown/document.rb +119 -0
  19. data/lib/kramdown/element.rb +524 -0
  20. data/lib/kramdown/error.rb +30 -0
  21. data/lib/kramdown/options.rb +373 -0
  22. data/lib/kramdown/parser.rb +39 -0
  23. data/lib/kramdown/parser/base.rb +136 -0
  24. data/lib/kramdown/parser/bean_kramdown.rb +25 -0
  25. data/lib/kramdown/parser/bean_kramdown/info_box.rb +52 -0
  26. data/lib/kramdown/parser/bean_kramdown/oembed.rb +230 -0
  27. data/lib/kramdown/parser/html.rb +570 -0
  28. data/lib/kramdown/parser/kramdown.rb +339 -0
  29. data/lib/kramdown/parser/kramdown/abbreviation.rb +71 -0
  30. data/lib/kramdown/parser/kramdown/autolink.rb +53 -0
  31. data/lib/kramdown/parser/kramdown/blank_line.rb +43 -0
  32. data/lib/kramdown/parser/kramdown/block_boundary.rb +46 -0
  33. data/lib/kramdown/parser/kramdown/blockquote.rb +51 -0
  34. data/lib/kramdown/parser/kramdown/codeblock.rb +63 -0
  35. data/lib/kramdown/parser/kramdown/codespan.rb +56 -0
  36. data/lib/kramdown/parser/kramdown/emphasis.rb +70 -0
  37. data/lib/kramdown/parser/kramdown/eob.rb +39 -0
  38. data/lib/kramdown/parser/kramdown/escaped_chars.rb +38 -0
  39. data/lib/kramdown/parser/kramdown/extensions.rb +204 -0
  40. data/lib/kramdown/parser/kramdown/footnote.rb +74 -0
  41. data/lib/kramdown/parser/kramdown/header.rb +68 -0
  42. data/lib/kramdown/parser/kramdown/horizontal_rule.rb +39 -0
  43. data/lib/kramdown/parser/kramdown/html.rb +169 -0
  44. data/lib/kramdown/parser/kramdown/html_entity.rb +44 -0
  45. data/lib/kramdown/parser/kramdown/image.rb +157 -0
  46. data/lib/kramdown/parser/kramdown/line_break.rb +38 -0
  47. data/lib/kramdown/parser/kramdown/link.rb +154 -0
  48. data/lib/kramdown/parser/kramdown/list.rb +240 -0
  49. data/lib/kramdown/parser/kramdown/math.rb +65 -0
  50. data/lib/kramdown/parser/kramdown/paragraph.rb +63 -0
  51. data/lib/kramdown/parser/kramdown/smart_quotes.rb +214 -0
  52. data/lib/kramdown/parser/kramdown/table.rb +178 -0
  53. data/lib/kramdown/parser/kramdown/typographic_symbol.rb +52 -0
  54. data/lib/kramdown/parser/markdown.rb +69 -0
  55. data/lib/kramdown/utils.rb +42 -0
  56. data/lib/kramdown/utils/entities.rb +348 -0
  57. data/lib/kramdown/utils/html.rb +85 -0
  58. data/lib/kramdown/utils/ordered_hash.rb +100 -0
  59. data/lib/kramdown/version.rb +28 -0
  60. 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}\">&#8901;</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\">&#8617;</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(/"/, "&quot;") + '"'
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(/"/, "&quot;") + '"' : '')
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(/"/, "&quot;") + '"' : ''}\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