Class: Kramdown::Converter::Html

Inherits:
Base
  • Object
show all
Includes:
Parser::Html::Constants, Utils::Html
Defined in:
lib/kramdown/converter/html.rb

Overview

Converts a Kramdown::Document to HTML.

You can customize the HTML converter by sub-classing it and overriding the convert_NAME methods. Each such method takes the following parameters:

el

The element of type NAME to be converted.

indent

A number representing the current amount of spaces for indent (only used for block-level elements).

The return value of such a method has to be a string containing the element el formatted as HTML element.

Constant Summary collapse

DISPATCHER =

The mapping of element type to conversion method.

Hash.new {|h,k| h[k] = "convert_#{k}"}
ENTITY_NBSP =

:nodoc:

::Kramdown::Utils::Entities.entity('nbsp')
TYPOGRAPHIC_SYMS =
{
  :mdash => [::Kramdown::Utils::Entities.entity('mdash')],
  :ndash => [::Kramdown::Utils::Entities.entity('ndash')],
  :hellip => [::Kramdown::Utils::Entities.entity('hellip')],
  :laquo_space => [::Kramdown::Utils::Entities.entity('laquo'), ::Kramdown::Utils::Entities.entity('nbsp')],
  :raquo_space => [::Kramdown::Utils::Entities.entity('nbsp'), ::Kramdown::Utils::Entities.entity('raquo')],
  :laquo => [::Kramdown::Utils::Entities.entity('laquo')],
  :raquo => [::Kramdown::Utils::Entities.entity('raquo')]
}
"%s<a href=\"#fnref:%s\" class=\"reversefootnote\">%s</a>"

Constants included from Parser::Html::Constants

Parser::Html::Constants::HTML_ATTRIBUTE_RE, Parser::Html::Constants::HTML_BLOCK_ELEMENTS, Parser::Html::Constants::HTML_COMMENT_RE, Parser::Html::Constants::HTML_CONTENT_MODEL, Parser::Html::Constants::HTML_CONTENT_MODEL_BLOCK, Parser::Html::Constants::HTML_CONTENT_MODEL_RAW, Parser::Html::Constants::HTML_CONTENT_MODEL_SPAN, Parser::Html::Constants::HTML_DOCTYPE_RE, Parser::Html::Constants::HTML_ELEMENTS_WITHOUT_BODY, Parser::Html::Constants::HTML_ENTITY_RE, Parser::Html::Constants::HTML_INSTRUCTION_RE, Parser::Html::Constants::HTML_SPAN_ELEMENTS, Parser::Html::Constants::HTML_TAG_CLOSE_RE, Parser::Html::Constants::HTML_TAG_RE

Constants included from Utils::Html

Utils::Html::ESCAPE_ALL_RE, Utils::Html::ESCAPE_ATTRIBUTE_RE, Utils::Html::ESCAPE_MAP, Utils::Html::ESCAPE_RE_FROM_TYPE, Utils::Html::ESCAPE_TEXT_RE

Constants inherited from Base

Base::SMART_QUOTE_INDICES

Instance Attribute Summary collapse

Attributes inherited from Base

#data, #options, #root, #warnings

Instance Method Summary collapse

Methods included from Utils::Html

#entity_to_str, #escape_html, #html_attributes

Methods inherited from Base

apply_template, #apply_template_after?, #apply_template_before?, convert, #extract_code_language, #extract_code_language!, #format_math, #generate_id, get_template, get_template_new, #highlight_code, #in_toc?, #output_header_level, #smart_quote_entity, #warning

Constructor Details

#initialize(root, options) ⇒ Html

Initialize the HTML converter with the given Kramdown document doc.



39
40
41
42
43
44
45
46
47
48
49
# File 'lib/kramdown/converter/html.rb', line 39

def initialize(root, options)
  super
  @footnote_counter = @footnote_start = @options[:footnote_nr]
  @footnotes = []
  @footnotes_by_name = {}
  @footnote_location = nil
  @toc = []
  @toc_code = nil
  @indent = 2
  @stack = []
end

Instance Attribute Details

#indentObject

The amount of indentation used when nesting HTML tags.



36
37
38
# File 'lib/kramdown/converter/html.rb', line 36

def indent
  @indent
end

Instance Method Details

#add_syntax_highlighter_to_class_attr(attr) ⇒ Object

Add the syntax highlighter name to the ‘class’ attribute of the given attribute hash.



366
367
368
# File 'lib/kramdown/converter/html.rb', line 366

def add_syntax_highlighter_to_class_attr(attr)
  (attr['class'] = (attr['class'] || '') + " highlighter-#{@options[:syntax_highlighter]}").lstrip!
end

#convert(el, indent = -@indent)) ⇒ Object

Dispatch the conversion of the element el to a convert_TYPE method using the type of the element.



56
57
58
# File 'lib/kramdown/converter/html.rb', line 56

def convert(el, indent = -@indent)
  send(DISPATCHER[el.type], el, indent)
end

#convert_a(el, indent) ⇒ Object



237
238
239
240
241
242
243
244
245
246
# File 'lib/kramdown/converter/html.rb', line 237

def convert_a(el, indent)
  res = inner(el, indent)
  attr = el.attr.dup
  if attr['href'].start_with?('mailto:')
    mail_addr = attr['href'][7..-1]
    attr['href'] = obfuscate('mailto') << ":" << obfuscate(mail_addr)
    res = obfuscate(res) if res == mail_addr
  end
  format_as_span_html(el.type, attr, res)
end

#convert_abbreviation(el, indent) ⇒ Object



323
324
325
326
327
328
# File 'lib/kramdown/converter/html.rb', line 323

def convert_abbreviation(el, indent)
  title = @root.options[:abbrev_defs][el.value]
  attr = @root.options[:abbrev_attr][el.value].dup
  attr['title'] = title unless title.empty?
  format_as_span_html("abbr", attr, el.value)
end

#convert_blank(el, indent) ⇒ Object



76
77
78
# File 'lib/kramdown/converter/html.rb', line 76

def convert_blank(el, indent)
  "\n"
end

#convert_blockquote(el, indent) ⇒ Object



120
121
122
# File 'lib/kramdown/converter/html.rb', line 120

def convert_blockquote(el, indent)
  format_as_indented_block_html(el.type, el.attr, inner(el, indent), indent)
end

#convert_br(el, indent) ⇒ Object



233
234
235
# File 'lib/kramdown/converter/html.rb', line 233

def convert_br(el, indent)
  "<br />"
end

#convert_codeblock(el, indent) ⇒ Object



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/kramdown/converter/html.rb', line 92

def convert_codeblock(el, indent)
  attr = el.attr.dup
  lang = extract_code_language!(attr)
  highlighted_code = highlight_code(el.value, lang, :block)

  if highlighted_code
    add_syntax_highlighter_to_class_attr(attr)
    "#{' '*indent}<div#{html_attributes(attr)}>#{highlighted_code}#{' '*indent}</div>\n"
  else
    result = escape_html(el.value)
    result.chomp!
    if el.attr['class'].to_s =~ /\bshow-whitespaces\b/
      result.gsub!(/(?:(^[ \t]+)|([ \t]+$)|([ \t]+))/) do |m|
        suffix = ($1 ? '-l' : ($2 ? '-r' : ''))
        m.scan(/./).map do |c|
          case c
          when "\t" then "<span class=\"ws-tab#{suffix}\">\t</span>"
          when " " then "<span class=\"ws-space#{suffix}\">&#8901;</span>"
          end
        end.join('')
      end
    end
    code_attr = {}
    code_attr['class'] = "language-#{lang}" if lang
    "#{' '*indent}<pre#{html_attributes(attr)}><code#{html_attributes(code_attr)}>#{result}\n</code></pre>\n"
  end
end

#convert_codespan(el, indent) ⇒ Object



252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/kramdown/converter/html.rb', line 252

def convert_codespan(el, indent)
  attr = el.attr.dup
  lang = extract_code_language(attr)
  result = highlight_code(el.value, lang, :span)
  if result
    add_syntax_highlighter_to_class_attr(attr)
  else
    result = escape_html(el.value)
  end

  format_as_span_html('code', attr, result)
end

#convert_comment(el, indent) ⇒ Object



225
226
227
228
229
230
231
# File 'lib/kramdown/converter/html.rb', line 225

def convert_comment(el, indent)
  if el.options[:category] == :block
    "#{' '*indent}<!-- #{el.value} -->\n"
  else
    "<!-- #{el.value} -->"
  end
end

#convert_dl(el, indent) ⇒ Object



150
151
152
# File 'lib/kramdown/converter/html.rb', line 150

def convert_dl(el, indent)
  format_as_indented_block_html(el.type, el.attr, inner(el, indent), indent)
end

#convert_dt(el, indent) ⇒ Object



166
167
168
# File 'lib/kramdown/converter/html.rb', line 166

def convert_dt(el, indent)
  format_as_block_html(el.type, el.attr, inner(el, indent), indent)
end

#convert_em(el, indent) ⇒ Object Also known as: convert_strong



287
288
289
# File 'lib/kramdown/converter/html.rb', line 287

def convert_em(el, indent)
  format_as_span_html(el.type, el.attr, inner(el, indent))
end

#convert_entity(el, indent) ⇒ Object



292
293
294
# File 'lib/kramdown/converter/html.rb', line 292

def convert_entity(el, indent)
  entity_to_str(el.value, el.options[:original])
end

#convert_footnote(el, indent) ⇒ Object



265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/kramdown/converter/html.rb', line 265

def convert_footnote(el, indent)
  repeat = ''
  if (footnote = @footnotes_by_name[el.options[:name]])
    number = footnote[2]
    repeat = ":#{footnote[3] += 1}"
  else
    number = @footnote_counter
    @footnote_counter += 1
    @footnotes << [el.options[:name], el.value, number, 0]
    @footnotes_by_name[el.options[:name]] = @footnotes.last
  end
  "<sup id=\"fnref:#{el.options[:name]}#{repeat}\"><a href=\"#fn:#{el.options[:name]}\" class=\"footnote\">#{number}</a></sup>"
end

#convert_header(el, indent) ⇒ Object



124
125
126
127
128
129
130
131
132
# File 'lib/kramdown/converter/html.rb', line 124

def convert_header(el, indent)
  attr = el.attr.dup
  if @options[:auto_ids] && !attr['id']
    attr['id'] = generate_id(el.options[:raw_text])
  end
  @toc << [el.options[:level], attr['id'], el.children] if attr['id'] && in_toc?(el)
  level = output_header_level(el.options[:level])
  format_as_block_html("h#{level}", attr, inner(el, indent), indent)
end

#convert_hr(el, indent) ⇒ Object



134
135
136
# File 'lib/kramdown/converter/html.rb', line 134

def convert_hr(el, indent)
  "#{' '*indent}<hr#{html_attributes(el.attr)} />\n"
end

#convert_html_element(el, indent) ⇒ Object



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/kramdown/converter/html.rb', line 170

def convert_html_element(el, indent)
  res = inner(el, indent)
  if el.options[:category] == :span
    "<#{el.value}#{html_attributes(el.attr)}" << (res.empty? && HTML_ELEMENTS_WITHOUT_BODY.include?(el.value) ? " />" : ">#{res}</#{el.value}>")
  else
    output = ''
    output << ' '*indent if @stack.last.type != :html_element || @stack.last.options[:content_model] != :raw
    output << "<#{el.value}#{html_attributes(el.attr)}"
    if el.options[:is_closed] && el.options[:content_model] == :raw
      output << " />"
    elsif !res.empty? && el.options[:content_model] != :block
      output << ">#{res}</#{el.value}>"
    elsif !res.empty?
      output << ">\n#{res.chomp}\n"  << ' '*indent << "</#{el.value}>"
    elsif HTML_ELEMENTS_WITHOUT_BODY.include?(el.value)
      output << " />"
    else
      output << "></#{el.value}>"
    end
    output << "\n" if @stack.last.type != :html_element || @stack.last.options[:content_model] != :raw
    output
  end
end

#convert_img(el, indent) ⇒ Object



248
249
250
# File 'lib/kramdown/converter/html.rb', line 248

def convert_img(el, indent)
  "<img#{html_attributes(el.attr)} />"
end

#convert_li(el, indent) ⇒ Object Also known as: convert_dd



154
155
156
157
158
159
160
161
162
163
# File 'lib/kramdown/converter/html.rb', line 154

def convert_li(el, indent)
  output = ' '*indent << "<#{el.type}" << html_attributes(el.attr) << ">"
  res = inner(el, indent)
  if el.children.empty? || (el.children.first.type == :p && el.children.first.options[:transparent])
    output << res << (res =~ /\n\Z/ ? ' '*indent : '')
  else
    output << "\n" << res << ' '*indent
  end
  output << "</#{el.type}>\n"
end

#convert_math(el, indent) ⇒ Object



313
314
315
316
317
318
319
320
321
# File 'lib/kramdown/converter/html.rb', line 313

def convert_math(el, indent)
  if (result = format_math(el, :indent => indent))
    result
  elsif el.options[:category] == :block
    format_as_block_html('pre', el.attr, "$$\n#{el.value}\n$$", indent)
  else
    format_as_span_html('span', el.attr, "$#{el.value}$")
  end
end

#convert_p(el, indent) ⇒ Object



84
85
86
87
88
89
90
# File 'lib/kramdown/converter/html.rb', line 84

def convert_p(el, indent)
  if el.options[:transparent]
    inner(el, indent)
  else
    format_as_block_html(el.type, el.attr, inner(el, indent), indent)
  end
end

#convert_raw(el, indent) ⇒ Object



279
280
281
282
283
284
285
# File 'lib/kramdown/converter/html.rb', line 279

def convert_raw(el, indent)
  if !el.options[:type] || el.options[:type].empty? || el.options[:type].include?('html')
    el.value + (el.options[:category] == :block ? "\n" : '')
  else
    ''
  end
end

#convert_root(el, indent) ⇒ Object



330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
# File 'lib/kramdown/converter/html.rb', line 330

def convert_root(el, indent)
  result = inner(el, indent)
  if @footnote_location
    result.sub!(/#{@footnote_location}/, footnote_content.gsub(/\\/, "\\\\\\\\"))
  else
    result << footnote_content
  end
  if @toc_code
    toc_tree = generate_toc_tree(@toc, @toc_code[0], @toc_code[1] || {})
    text = if toc_tree.children.size > 0
             convert(toc_tree, 0)
           else
             ''
           end
    result.sub!(/#{@toc_code.last}/, text.gsub(/\\/, "\\\\\\\\"))
  end
  result
end

#convert_smart_quote(el, indent) ⇒ Object



309
310
311
# File 'lib/kramdown/converter/html.rb', line 309

def convert_smart_quote(el, indent)
  entity_to_str(smart_quote_entity(el))
end

#convert_table(el, indent) ⇒ Object Also known as: convert_thead, convert_tbody, convert_tfoot, convert_tr



203
204
205
# File 'lib/kramdown/converter/html.rb', line 203

def convert_table(el, indent)
  format_as_indented_block_html(el.type, el.attr, inner(el, indent), indent)
end

#convert_td(el, indent) ⇒ Object



213
214
215
216
217
218
219
220
221
222
223
# File 'lib/kramdown/converter/html.rb', line 213

def convert_td(el, indent)
  res = inner(el, indent)
  type = (@stack[-2].type == :thead ? :th : :td)
  attr = el.attr
  alignment = @stack[-3].options[:alignment][@stack.last.children.index(el)]
  if alignment != :default
    attr = el.attr.dup
    attr['style'] = (attr.has_key?('style') ? "#{attr['style']}; ": '') << "text-align: #{alignment}"
  end
  format_as_block_html(type, attr, res.empty? ? entity_to_str(ENTITY_NBSP) : res, indent)
end

#convert_text(el, indent) ⇒ Object



80
81
82
# File 'lib/kramdown/converter/html.rb', line 80

def convert_text(el, indent)
  escape_html(el.value, :text)
end

#convert_typographic_sym(el, indent) ⇒ Object

:nodoc:



305
306
307
# File 'lib/kramdown/converter/html.rb', line 305

def convert_typographic_sym(el, indent)
  TYPOGRAPHIC_SYMS[el.value].map {|e| entity_to_str(e)}.join('')
end

#convert_ul(el, indent) ⇒ Object Also known as: convert_ol



138
139
140
141
142
143
144
145
146
147
# File 'lib/kramdown/converter/html.rb', line 138

def convert_ul(el, indent)
  if !@toc_code && (el.options[:ial][:refs].include?('toc') rescue nil)
    @toc_code = [el.type, el.attr, (0..128).to_a.map{|a| rand(36).to_s(36)}.join]
    @toc_code.last
  elsif !@footnote_location && el.options[:ial] && (el.options[:ial][:refs] || []).include?('footnotes')
    @footnote_location = (0..128).to_a.map{|a| rand(36).to_s(36)}.join
  else
    format_as_indented_block_html(el.type, el.attr, inner(el, indent), indent)
  end
end

#convert_xml_comment(el, indent) ⇒ Object Also known as: convert_xml_pi



194
195
196
197
198
199
200
# File 'lib/kramdown/converter/html.rb', line 194

def convert_xml_comment(el, indent)
  if el.options[:category] == :block && (@stack.last.type != :html_element || @stack.last.options[:content_model] != :raw)
    ' '*indent << el.value << "\n"
  else
    el.value
  end
end

#footnote_contentObject

Return a HTML ordered list with the footnote content for the used footnotes.



429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
# File 'lib/kramdown/converter/html.rb', line 429

def footnote_content
  ol = Element.new(:ol)
  ol.attr['start'] = @footnote_start if @footnote_start != 1
  i = 0
  backlink_text = escape_html(@options[:footnote_backlink], :text)
  while i < @footnotes.length
    name, data, _, repeat = *@footnotes[i]
    li = Element.new(:li, nil, {'id' => "fn:#{name}"})
    li.children = Marshal.load(Marshal.dump(data.children))

    if li.children.last.type == :p
      para = li.children.last
      insert_space = true
    else
      li.children << (para = Element.new(:p))
      insert_space = false
    end

    unless @options[:footnote_backlink].empty?
      para.children << Element.new(:raw, FOOTNOTE_BACKLINK_FMT % [insert_space ? ' ' : '', name, backlink_text])
      (1..repeat).each do |index|
        para.children << Element.new(:raw, FOOTNOTE_BACKLINK_FMT % [" ", "#{name}:#{index}", "#{backlink_text}<sup>#{index+1}</sup>"])
      end
    end

    ol.children << Element.new(:raw, convert(li, 4))
    i += 1
  end
  (ol.children.empty? ? '' : format_as_indented_block_html('div', {:class => "footnotes"}, convert(ol, 2), 0))
end

#format_as_block_html(name, attr, body, indent) ⇒ Object

Format the given element as block HTML.



355
356
357
# File 'lib/kramdown/converter/html.rb', line 355

def format_as_block_html(name, attr, body, indent)
  "#{' '*indent}<#{name}#{html_attributes(attr)}>#{body}</#{name}>\n"
end

#format_as_indented_block_html(name, attr, body, indent) ⇒ Object

Format the given element as block HTML with a newline after the start tag and indentation before the end tag.



361
362
363
# File 'lib/kramdown/converter/html.rb', line 361

def format_as_indented_block_html(name, attr, body, indent)
  "#{' '*indent}<#{name}#{html_attributes(attr)}>\n#{body}#{' '*indent}</#{name}>\n"
end

#format_as_span_html(name, attr, body) ⇒ Object

Format the given element as span HTML.



350
351
352
# File 'lib/kramdown/converter/html.rb', line 350

def format_as_span_html(name, attr, body)
  "<#{name}#{html_attributes(attr)}>#{body}</#{name}>"
end

#generate_toc_tree(toc, type, attr) ⇒ Object

Generate and return an element tree for the table of contents.



371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
# File 'lib/kramdown/converter/html.rb', line 371

def generate_toc_tree(toc, type, attr)
  sections = Element.new(type, nil, attr)
  sections.attr['id'] ||= 'markdown-toc'
  stack = []
  toc.each do |level, id, children|
    li = Element.new(:li, nil, nil, {:level => level})
    li.children << Element.new(:p, nil, nil, {:transparent => true})
    a = Element.new(:a, nil)
    a.attr['href'] = "##{id}"
    a.attr['id'] = "#{sections.attr['id']}-#{id}"
    a.children.concat(remove_footnotes(Marshal.load(Marshal.dump(children))))
    li.children.last.children << a
    li.children << Element.new(type)

    success = false
    while !success
      if stack.empty?
        sections.children << li
        stack << li
        success = true
      elsif stack.last.options[:level] < li.options[:level]
        stack.last.children.last.children << li
        stack << li
        success = true
      else
        item = stack.pop
        item.children.pop unless item.children.last.children.size > 0
      end
    end
  end
  while !stack.empty?
    item = stack.pop
    item.children.pop unless item.children.last.children.size > 0
  end
  sections
end

#inner(el, indent) ⇒ Object

Return the converted content of the children of el as a string. The parameter indent has to be the amount of indentation used for the element el.

Pushes el onto the @stack before converting the child elements and pops it from the stack afterwards.



65
66
67
68
69
70
71
72
73
74
# File 'lib/kramdown/converter/html.rb', line 65

def inner(el, indent)
  result = ''
  indent += @indent
  @stack.push(el)
  el.children.each do |inner_el|
    result << send(DISPATCHER[inner_el.type], inner_el, indent)
  end
  @stack.pop
  result
end

#obfuscate(text) ⇒ Object

Obfuscate the text by using HTML entities.



417
418
419
420
421
422
423
424
# File 'lib/kramdown/converter/html.rb', line 417

def obfuscate(text)
  result = ""
  text.each_byte do |b|
    result << (b > 128 ? b.chr : "&#%03d;" % b)
  end
  result.force_encoding(text.encoding) if result.respond_to?(:force_encoding)
  result
end

#remove_footnotes(elements) ⇒ Object

Remove all footnotes from the given elements.



409
410
411
412
413
414
# File 'lib/kramdown/converter/html.rb', line 409

def remove_footnotes(elements)
  elements.delete_if do |c|
    remove_footnotes(c.children)
    c.type == :footnote
  end
end