class Kramdown::Converter::Html
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.
Constants
- FOOTNOTE_BACKLINK_FMT
- ZERO_TO_ONETWENTYEIGHT
Attributes
The amount of indentation used when nesting HTML tags.
Public Class Methods
Initialize the HTML converter with the given Kramdown
document doc
.
Kramdown::Converter::Base::new
# File lib/kramdown/converter/html.rb 38 def initialize(root, options) 39 super 40 @footnote_counter = @footnote_start = @options[:footnote_nr] 41 @footnotes = [] 42 @footnotes_by_name = {} 43 @footnote_location = nil 44 @toc = [] 45 @toc_code = nil 46 @indent = 2 47 @stack = [] 48 49 # stash string representation of symbol to avoid allocations from multiple interpolations. 50 @highlighter_class = " highlighter-#{options[:syntax_highlighter]}" 51 @dispatcher = Hash.new {|h, k| h[k] = :"convert_#{k}" } 52 end
Public Instance Methods
Add the syntax highlighter name to the 'class' attribute of the given attribute hash. And overwrites or add a “language-LANG” part using the lang
parameter if lang
is not nil.
# File lib/kramdown/converter/html.rb 408 def add_syntax_highlighter_to_class_attr(attr, lang = nil) 409 (attr['class'] = (attr['class'] || '') + @highlighter_class).lstrip! 410 attr['class'].sub!(/\blanguage-\S+|(^)/) { "language-#{lang}#{$1 ? ' ' : ''}" } if lang 411 end
Dispatch the conversion of the element el
to a convert_TYPE
method using the type
of the element.
# File lib/kramdown/converter/html.rb 56 def convert(el, indent = -@indent) 57 send(@dispatcher[el.type], el, indent) 58 end
# File lib/kramdown/converter/html.rb 271 def convert_a(el, indent) 272 format_as_span_html("a", el.attr, inner(el, indent)) 273 end
# File lib/kramdown/converter/html.rb 364 def convert_abbreviation(el, _indent) 365 title = @root.options[:abbrev_defs][el.value] 366 attr = @root.options[:abbrev_attr][el.value].dup 367 attr['title'] = title unless title.empty? 368 format_as_span_html("abbr", attr, el.value) 369 end
# File lib/kramdown/converter/html.rb 76 def convert_blank(_el, _indent) 77 "\n" 78 end
# File lib/kramdown/converter/html.rb 140 def convert_blockquote(el, indent) 141 format_as_indented_block_html("blockquote", el.attr, inner(el, indent), indent) 142 end
# File lib/kramdown/converter/html.rb 267 def convert_br(_el, _indent) 268 "<br />" 269 end
# File lib/kramdown/converter/html.rb 110 def convert_codeblock(el, indent) 111 attr = el.attr.dup 112 lang = extract_code_language!(attr) 113 hl_opts = {} 114 highlighted_code = highlight_code(el.value, el.options[:lang] || lang, :block, hl_opts) 115 116 if highlighted_code 117 add_syntax_highlighter_to_class_attr(attr, lang || hl_opts[:default_lang]) 118 "#{' ' * indent}<div#{html_attributes(attr)}>#{highlighted_code}#{' ' * indent}</div>\n" 119 else 120 result = escape_html(el.value) 121 result.chomp! 122 if el.attr['class'].to_s =~ /\bshow-whitespaces\b/ 123 result.gsub!(/(?:(^[ \t]+)|([ \t]+$)|([ \t]+))/) do |m| 124 suffix = ($1 ? '-l' : ($2 ? '-r' : '')) 125 m.scan(/./).map do |c| 126 case c 127 when "\t" then "<span class=\"ws-tab#{suffix}\">\t</span>" 128 when " " then "<span class=\"ws-space#{suffix}\">⋅</span>" 129 end 130 end.join('') 131 end 132 end 133 code_attr = {} 134 code_attr['class'] = "language-#{lang}" if lang 135 "#{' ' * indent}<pre#{html_attributes(attr)}>" \ 136 "<code#{html_attributes(code_attr)}>#{result}\n</code></pre>\n" 137 end 138 end
# File lib/kramdown/converter/html.rb 279 def convert_codespan(el, _indent) 280 attr = el.attr.dup 281 lang = extract_code_language(attr) 282 hl_opts = {} 283 result = highlight_code(el.value, lang, :span, hl_opts) 284 if result 285 add_syntax_highlighter_to_class_attr(attr, lang || hl_opts[:default_lang]) 286 else 287 result = escape_html(el.value) 288 end 289 290 format_as_span_html('code', attr, result) 291 end
# File lib/kramdown/converter/html.rb 259 def convert_comment(el, indent) 260 if el.options[:category] == :block 261 "#{' ' * indent}<!-- #{el.value} -->\n" 262 else 263 "<!-- #{el.value} -->" 264 end 265 end
# File lib/kramdown/converter/html.rb 173 def convert_dl(el, indent) 174 format_as_indented_block_html("dl", el.attr, inner(el, indent), indent) 175 end
# File lib/kramdown/converter/html.rb 189 def convert_dt(el, indent) 190 attr = el.attr.dup 191 @stack.last.options[:ial][:refs].each do |ref| 192 if ref =~ /\Aauto_ids(?:-([\w-]+))?/ 193 attr['id'] = "#{$1}#{basic_generate_id(el.options[:raw_text])}".lstrip 194 break 195 end 196 end if !attr['id'] && @stack.last.options[:ial] && @stack.last.options[:ial][:refs] 197 format_as_block_html("dt", attr, inner(el, indent), indent) 198 end
# File lib/kramdown/converter/html.rb 318 def convert_em(el, indent) 319 format_as_span_html(el.type, el.attr, inner(el, indent)) 320 end
# File lib/kramdown/converter/html.rb 323 def convert_entity(el, _indent) 324 entity_to_str(el.value, el.options[:original]) 325 end
# File lib/kramdown/converter/html.rb 293 def convert_footnote(el, _indent) 294 repeat = '' 295 name = @options[:footnote_prefix] + el.options[:name] 296 if (footnote = @footnotes_by_name[name]) 297 number = footnote[2] 298 repeat = ":#{footnote[3] += 1}" 299 else 300 number = @footnote_counter 301 @footnote_counter += 1 302 @footnotes << [name, el.value, number, 0] 303 @footnotes_by_name[name] = @footnotes.last 304 end 305 "<sup id=\"fnref:#{name}#{repeat}\" role=\"doc-noteref\">" \ 306 "<a href=\"#fn:#{name}\" class=\"footnote\" rel=\"footnote\">" \ 307 "#{number}</a></sup>" 308 end
# File lib/kramdown/converter/html.rb 144 def convert_header(el, indent) 145 attr = el.attr.dup 146 if @options[:auto_ids] && !attr['id'] 147 attr['id'] = generate_id(el.options[:raw_text]) 148 end 149 @toc << [el.options[:level], attr['id'], el.children] if attr['id'] && in_toc?(el) 150 level = output_header_level(el.options[:level]) 151 format_as_block_html("h#{level}", attr, inner(el, indent), indent) 152 end
# File lib/kramdown/converter/html.rb 154 def convert_hr(el, indent) 155 "#{' ' * indent}<hr#{html_attributes(el.attr)} />\n" 156 end
# File lib/kramdown/converter/html.rb 200 def convert_html_element(el, indent) 201 res = inner(el, indent) 202 if el.options[:category] == :span 203 "<#{el.value}#{html_attributes(el.attr)}" + \ 204 (res.empty? && HTML_ELEMENTS_WITHOUT_BODY.include?(el.value) ? " />" : ">#{res}</#{el.value}>") 205 else 206 output = +'' 207 if @stack.last.type != :html_element || @stack.last.options[:content_model] != :raw 208 output << ' ' * indent 209 end 210 output << "<#{el.value}#{html_attributes(el.attr)}" 211 if el.options[:is_closed] && el.options[:content_model] == :raw 212 output << " />" 213 elsif !res.empty? && el.options[:content_model] != :block 214 output << ">#{res}</#{el.value}>" 215 elsif !res.empty? 216 output << ">\n#{res.chomp}\n" << ' ' * indent << "</#{el.value}>" 217 elsif HTML_ELEMENTS_WITHOUT_BODY.include?(el.value) 218 output << " />" 219 else 220 output << "></#{el.value}>" 221 end 222 output << "\n" if @stack.last.type != :html_element || @stack.last.options[:content_model] != :raw 223 output 224 end 225 end
# File lib/kramdown/converter/html.rb 275 def convert_img(el, _indent) 276 "<img#{html_attributes(el.attr)} />" 277 end
# File lib/kramdown/converter/html.rb 177 def convert_li(el, indent) 178 output = ' ' * indent << "<#{el.type}" << html_attributes(el.attr) << ">" 179 res = inner(el, indent) 180 if el.children.empty? || (el.children.first.type == :p && el.children.first.options[:transparent]) 181 output << res << (res =~ /\n\Z/ ? ' ' * indent : '') 182 else 183 output << "\n" << res << ' ' * indent 184 end 185 output << "</#{el.type}>\n" 186 end
# File lib/kramdown/converter/html.rb 350 def convert_math(el, indent) 351 if (result = format_math(el, indent: indent)) 352 result 353 else 354 attr = el.attr.dup 355 attr['class'] = "#{attr['class']} kdmath".lstrip 356 if el.options[:category] == :block 357 format_as_block_html('div', attr, "$$\n#{el.value}\n$$", indent) 358 else 359 format_as_span_html('span', attr, "$#{el.value}$") 360 end 361 end 362 end
# File lib/kramdown/converter/html.rb 85 def convert_p(el, indent) 86 if el.options[:transparent] 87 inner(el, indent) 88 elsif el.children.size == 1 && el.children.first.type == :img && 89 el.children.first.options[:ial]&.[](:refs)&.include?('standalone') 90 convert_standalone_image(el, indent) 91 else 92 format_as_block_html("p", el.attr, inner(el, indent), indent) 93 end 94 end
# File lib/kramdown/converter/html.rb 310 def convert_raw(el, _indent) 311 if !el.options[:type] || el.options[:type].empty? || el.options[:type].include?('html') 312 el.value + (el.options[:category] == :block ? "\n" : '') 313 else 314 '' 315 end 316 end
# File lib/kramdown/converter/html.rb 371 def convert_root(el, indent) 372 result = inner(el, indent) 373 if @footnote_location 374 result.sub!(/#{@footnote_location}/, footnote_content.gsub(/\\/, "\\\\\\\\")) 375 else 376 result << footnote_content 377 end 378 if @toc_code 379 toc_tree = generate_toc_tree(@toc, @toc_code[0], @toc_code[1] || {}) 380 text = if !toc_tree.children.empty? 381 convert(toc_tree, 0) 382 else 383 '' 384 end 385 result.sub!(/#{@toc_code.last}/, text.gsub(/\\/, "\\\\\\\\")) 386 end 387 result 388 end
# File lib/kramdown/converter/html.rb 346 def convert_smart_quote(el, _indent) 347 entity_to_str(smart_quote_entity(el)) 348 end
Helper method used by convert_p
to convert a paragraph that only contains a single :img element.
# File lib/kramdown/converter/html.rb 98 def convert_standalone_image(el, indent) 99 figure_attr = el.attr.dup 100 image_attr = el.children.first.attr.dup 101 102 figure_attr['class'] = image_attr.delete('class') if image_attr.key?('class') and not figure_attr.key?('class') 103 figure_attr['id'] = image_attr.delete('id') if image_attr.key?('id') and not figure_attr.key?('id') 104 105 body = "#{' ' * (indent + @indent)}<img#{html_attributes(image_attr)} />\n" \ 106 "#{' ' * (indent + @indent)}<figcaption>#{image_attr['alt']}</figcaption>\n" 107 format_as_indented_block_html("figure", figure_attr, body, indent) 108 end
# File lib/kramdown/converter/html.rb 237 def convert_table(el, indent) 238 format_as_indented_block_html(el.type, el.attr, inner(el, indent), indent) 239 end
# File lib/kramdown/converter/html.rb 247 def convert_td(el, indent) 248 res = inner(el, indent) 249 type = (@stack[-2].type == :thead ? :th : :td) 250 attr = el.attr 251 alignment = @stack[-3].options[:alignment][@stack.last.children.index(el)] 252 if alignment != :default 253 attr = el.attr.dup 254 attr['style'] = (attr.key?('style') ? "#{attr['style']}; " : '') + "text-align: #{alignment}" 255 end 256 format_as_block_html(type, attr, res.empty? ? entity_to_str(ENTITY_NBSP) : res, indent) 257 end
# File lib/kramdown/converter/html.rb 80 def convert_text(el, _indent) 81 escaped = escape_html(el.value, :text) 82 @options[:remove_line_breaks_for_cjk] ? fix_cjk_line_break(escaped) : escaped 83 end
# File lib/kramdown/converter/html.rb 338 def convert_typographic_sym(el, _indent) 339 if (result = @options[:typographic_symbols][el.value]) 340 escape_html(result, :text) 341 else 342 TYPOGRAPHIC_SYMS[el.value].map {|e| entity_to_str(e) }.join('') 343 end 344 end
# File lib/kramdown/converter/html.rb 161 def convert_ul(el, indent) 162 if !@toc_code && el.options.dig(:ial, :refs)&.include?('toc') 163 @toc_code = [el.type, el.attr, ZERO_TO_ONETWENTYEIGHT.map { rand(36).to_s(36) }.join] 164 @toc_code.last 165 elsif !@footnote_location && el.options.dig(:ial, :refs)&.include?('footnotes') 166 @footnote_location = ZERO_TO_ONETWENTYEIGHT.map { rand(36).to_s(36) }.join 167 else 168 format_as_indented_block_html(el.type, el.attr, inner(el, indent), indent) 169 end 170 end
# File lib/kramdown/converter/html.rb 227 def convert_xml_comment(el, indent) 228 if el.options[:category] == :block && 229 (@stack.last.type != :html_element || @stack.last.options[:content_model] != :raw) 230 ' ' * indent << el.value << "\n" 231 else 232 el.value 233 end 234 end
Fixes the elements for use in a TOC entry.
# File lib/kramdown/converter/html.rb 452 def fix_for_toc_entry(elements) 453 remove_footnotes(elements) 454 unwrap_links(elements) 455 elements 456 end
Return an HTML ordered list with the footnote content for the used footnotes.
# File lib/kramdown/converter/html.rb 487 def footnote_content 488 ol = Element.new(:ol) 489 ol.attr['start'] = @footnote_start if @footnote_start != 1 490 i = 0 491 backlink_text = escape_html(@options[:footnote_backlink], :text) 492 while i < @footnotes.length 493 name, data, _, repeat = *@footnotes[i] 494 li = Element.new(:li, nil, 'id' => "fn:#{name}", 'role' => 'doc-endnote') 495 li.children = Marshal.load(Marshal.dump(data.children)) 496 497 para = nil 498 if li.children.last.type == :p || @options[:footnote_backlink_inline] 499 parent = li 500 while !parent.children.empty? && ![:p, :header].include?(parent.children.last.type) 501 parent = parent.children.last 502 end 503 para = parent.children.last 504 insert_space = true 505 end 506 507 unless para 508 li.children << (para = Element.new(:p)) 509 insert_space = false 510 end 511 512 unless @options[:footnote_backlink].empty? 513 nbsp = entity_to_str(ENTITY_NBSP) 514 value = sprintf(FOOTNOTE_BACKLINK_FMT, (insert_space ? nbsp : ''), name, backlink_text) 515 para.children << Element.new(:raw, value) 516 (1..repeat).each do |index| 517 value = sprintf(FOOTNOTE_BACKLINK_FMT, nbsp, "#{name}:#{index}", 518 "#{backlink_text}<sup>#{index + 1}</sup>") 519 para.children << Element.new(:raw, value) 520 end 521 end 522 523 ol.children << Element.new(:raw, convert(li, 4)) 524 i += 1 525 end 526 if ol.children.empty? 527 '' 528 else 529 format_as_indented_block_html('div', {class: "footnotes", role: "doc-endnotes"}, convert(ol, 2), 0) 530 end 531 end
Format the given element as block HTML.
# File lib/kramdown/converter/html.rb 396 def format_as_block_html(name, attr, body, indent) 397 "#{' ' * indent}<#{name}#{html_attributes(attr)}>#{body}</#{name}>\n" 398 end
Format the given element as block HTML with a newline after the start tag and indentation before the end tag.
# File lib/kramdown/converter/html.rb 402 def format_as_indented_block_html(name, attr, body, indent) 403 "#{' ' * indent}<#{name}#{html_attributes(attr)}>\n#{body}#{' ' * indent}</#{name}>\n" 404 end
Format the given element as span HTML.
# File lib/kramdown/converter/html.rb 391 def format_as_span_html(name, attr, body) 392 "<#{name}#{html_attributes(attr)}>#{body}</#{name}>" 393 end
Generate and return an element tree for the table of contents.
# File lib/kramdown/converter/html.rb 414 def generate_toc_tree(toc, type, attr) 415 sections = Element.new(type, nil, attr.dup) 416 sections.attr['id'] ||= 'markdown-toc' 417 stack = [] 418 toc.each do |level, id, children| 419 li = Element.new(:li, nil, nil, level: level) 420 li.children << Element.new(:p, nil, nil, transparent: true) 421 a = Element.new(:a, nil) 422 a.attr['href'] = "##{id}" 423 a.attr['id'] = "#{sections.attr['id']}-#{id}" 424 a.children.concat(fix_for_toc_entry(Marshal.load(Marshal.dump(children)))) 425 li.children.last.children << a 426 li.children << Element.new(type) 427 428 success = false 429 until success 430 if stack.empty? 431 sections.children << li 432 stack << li 433 success = true 434 elsif stack.last.options[:level] < li.options[:level] 435 stack.last.children.last.children << li 436 stack << li 437 success = true 438 else 439 item = stack.pop 440 item.children.pop if item.children.last.children.empty? 441 end 442 end 443 end 444 until stack.empty? 445 item = stack.pop 446 item.children.pop if item.children.last.children.empty? 447 end 448 sections 449 end
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.
# File lib/kramdown/converter/html.rb 65 def inner(el, indent) 66 result = +'' 67 indent += @indent 68 @stack.push(el) 69 el.children.each do |inner_el| 70 result << send(@dispatcher[inner_el.type], inner_el, indent) 71 end 72 @stack.pop 73 result 74 end
Obfuscate the text
by using HTML entities.
# File lib/kramdown/converter/html.rb 475 def obfuscate(text) 476 result = +'' 477 text.each_byte do |b| 478 result << (b > 128 ? b.chr : sprintf("&#%03d;", b)) 479 end 480 result.force_encoding(text.encoding) 481 result 482 end
Remove all footnotes from the given elements.
# File lib/kramdown/converter/html.rb 467 def remove_footnotes(elements) 468 elements.delete_if do |c| 469 remove_footnotes(c.children) 470 c.type == :footnote 471 end 472 end
Remove all link elements by unwrapping them.
# File lib/kramdown/converter/html.rb 459 def unwrap_links(elements) 460 elements.map! do |c| 461 unwrap_links(c.children) 462 c.type == :a ? c.children : c 463 end.flatten! 464 end