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 406 def add_syntax_highlighter_to_class_attr(attr, lang = nil) 407 (attr['class'] = (attr['class'] || '') + @highlighter_class).lstrip! 408 attr['class'].sub!(/\blanguage-\S+|(^)/) { "language-#{lang}#{$1 ? ' ' : ''}" } if lang 409 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 269 def convert_a(el, indent) 270 format_as_span_html("a", el.attr, inner(el, indent)) 271 end
# File lib/kramdown/converter/html.rb 362 def convert_abbreviation(el, _indent) 363 title = @root.options[:abbrev_defs][el.value] 364 attr = @root.options[:abbrev_attr][el.value].dup 365 attr['title'] = title unless title.empty? 366 format_as_span_html("abbr", attr, el.value) 367 end
# File lib/kramdown/converter/html.rb 76 def convert_blank(_el, _indent) 77 "\n" 78 end
# File lib/kramdown/converter/html.rb 138 def convert_blockquote(el, indent) 139 format_as_indented_block_html("blockquote", el.attr, inner(el, indent), indent) 140 end
# File lib/kramdown/converter/html.rb 265 def convert_br(_el, _indent) 266 "<br />" 267 end
# File lib/kramdown/converter/html.rb 108 def convert_codeblock(el, indent) 109 attr = el.attr.dup 110 lang = extract_code_language!(attr) 111 hl_opts = {} 112 highlighted_code = highlight_code(el.value, el.options[:lang] || lang, :block, hl_opts) 113 114 if highlighted_code 115 add_syntax_highlighter_to_class_attr(attr, lang || hl_opts[:default_lang]) 116 "#{' ' * indent}<div#{html_attributes(attr)}>#{highlighted_code}#{' ' * indent}</div>\n" 117 else 118 result = escape_html(el.value) 119 result.chomp! 120 if el.attr['class'].to_s =~ /\bshow-whitespaces\b/ 121 result.gsub!(/(?:(^[ \t]+)|([ \t]+$)|([ \t]+))/) do |m| 122 suffix = ($1 ? '-l' : ($2 ? '-r' : '')) 123 m.scan(/./).map do |c| 124 case c 125 when "\t" then "<span class=\"ws-tab#{suffix}\">\t</span>" 126 when " " then "<span class=\"ws-space#{suffix}\">⋅</span>" 127 end 128 end.join('') 129 end 130 end 131 code_attr = {} 132 code_attr['class'] = "language-#{lang}" if lang 133 "#{' ' * indent}<pre#{html_attributes(attr)}>" \ 134 "<code#{html_attributes(code_attr)}>#{result}\n</code></pre>\n" 135 end 136 end
# File lib/kramdown/converter/html.rb 277 def convert_codespan(el, _indent) 278 attr = el.attr.dup 279 lang = extract_code_language(attr) 280 hl_opts = {} 281 result = highlight_code(el.value, lang, :span, hl_opts) 282 if result 283 add_syntax_highlighter_to_class_attr(attr, hl_opts[:default_lang]) 284 else 285 result = escape_html(el.value) 286 end 287 288 format_as_span_html('code', attr, result) 289 end
# File lib/kramdown/converter/html.rb 257 def convert_comment(el, indent) 258 if el.options[:category] == :block 259 "#{' ' * indent}<!-- #{el.value} -->\n" 260 else 261 "<!-- #{el.value} -->" 262 end 263 end
# File lib/kramdown/converter/html.rb 171 def convert_dl(el, indent) 172 format_as_indented_block_html("dl", el.attr, inner(el, indent), indent) 173 end
# File lib/kramdown/converter/html.rb 187 def convert_dt(el, indent) 188 attr = el.attr.dup 189 @stack.last.options[:ial][:refs].each do |ref| 190 if ref =~ /\Aauto_ids(?:-([\w-]+))?/ 191 attr['id'] = "#{$1}#{basic_generate_id(el.options[:raw_text])}".lstrip 192 break 193 end 194 end if !attr['id'] && @stack.last.options[:ial] && @stack.last.options[:ial][:refs] 195 format_as_block_html("dt", attr, inner(el, indent), indent) 196 end
# File lib/kramdown/converter/html.rb 316 def convert_em(el, indent) 317 format_as_span_html(el.type, el.attr, inner(el, indent)) 318 end
# File lib/kramdown/converter/html.rb 321 def convert_entity(el, _indent) 322 entity_to_str(el.value, el.options[:original]) 323 end
# File lib/kramdown/converter/html.rb 291 def convert_footnote(el, _indent) 292 repeat = '' 293 name = @options[:footnote_prefix] + el.options[:name] 294 if (footnote = @footnotes_by_name[name]) 295 number = footnote[2] 296 repeat = ":#{footnote[3] += 1}" 297 else 298 number = @footnote_counter 299 @footnote_counter += 1 300 @footnotes << [name, el.value, number, 0] 301 @footnotes_by_name[name] = @footnotes.last 302 end 303 "<sup id=\"fnref:#{name}#{repeat}\" role=\"doc-noteref\">" \ 304 "<a href=\"#fn:#{name}\" class=\"footnote\">" \ 305 "#{number}</a></sup>" 306 end
# File lib/kramdown/converter/html.rb 142 def convert_header(el, indent) 143 attr = el.attr.dup 144 if @options[:auto_ids] && !attr['id'] 145 attr['id'] = generate_id(el.options[:raw_text]) 146 end 147 @toc << [el.options[:level], attr['id'], el.children] if attr['id'] && in_toc?(el) 148 level = output_header_level(el.options[:level]) 149 format_as_block_html("h#{level}", attr, inner(el, indent), indent) 150 end
# File lib/kramdown/converter/html.rb 152 def convert_hr(el, indent) 153 "#{' ' * indent}<hr#{html_attributes(el.attr)} />\n" 154 end
# File lib/kramdown/converter/html.rb 198 def convert_html_element(el, indent) 199 res = inner(el, indent) 200 if el.options[:category] == :span 201 "<#{el.value}#{html_attributes(el.attr)}" + \ 202 (res.empty? && HTML_ELEMENTS_WITHOUT_BODY.include?(el.value) ? " />" : ">#{res}</#{el.value}>") 203 else 204 output = +'' 205 if @stack.last.type != :html_element || @stack.last.options[:content_model] != :raw 206 output << ' ' * indent 207 end 208 output << "<#{el.value}#{html_attributes(el.attr)}" 209 if el.options[:is_closed] && el.options[:content_model] == :raw 210 output << " />" 211 elsif !res.empty? && el.options[:content_model] != :block 212 output << ">#{res}</#{el.value}>" 213 elsif !res.empty? 214 output << ">\n#{res.chomp}\n" << ' ' * indent << "</#{el.value}>" 215 elsif HTML_ELEMENTS_WITHOUT_BODY.include?(el.value) 216 output << " />" 217 else 218 output << "></#{el.value}>" 219 end 220 output << "\n" if @stack.last.type != :html_element || @stack.last.options[:content_model] != :raw 221 output 222 end 223 end
# File lib/kramdown/converter/html.rb 273 def convert_img(el, _indent) 274 "<img#{html_attributes(el.attr)} />" 275 end
# File lib/kramdown/converter/html.rb 175 def convert_li(el, indent) 176 output = ' ' * indent << "<#{el.type}" << html_attributes(el.attr) << ">" 177 res = inner(el, indent) 178 if el.children.empty? || (el.children.first.type == :p && el.children.first.options[:transparent]) 179 output << res << (res =~ /\n\Z/ ? ' ' * indent : '') 180 else 181 output << "\n" << res << ' ' * indent 182 end 183 output << "</#{el.type}>\n" 184 end
# File lib/kramdown/converter/html.rb 348 def convert_math(el, indent) 349 if (result = format_math(el, indent: indent)) 350 result 351 else 352 attr = el.attr.dup 353 attr['class'] = "#{attr['class']} kdmath".lstrip 354 if el.options[:category] == :block 355 format_as_block_html('div', attr, "$$\n#{el.value}\n$$", indent) 356 else 357 format_as_span_html('span', attr, "$#{el.value}$") 358 end 359 end 360 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.children.first, 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 308 def convert_raw(el, _indent) 309 if !el.options[:type] || el.options[:type].empty? || el.options[:type].include?('html') 310 el.value + (el.options[:category] == :block ? "\n" : '') 311 else 312 '' 313 end 314 end
# File lib/kramdown/converter/html.rb 369 def convert_root(el, indent) 370 result = inner(el, indent) 371 if @footnote_location 372 result.sub!(/#{@footnote_location}/, footnote_content.gsub(/\\/, "\\\\\\\\")) 373 else 374 result << footnote_content 375 end 376 if @toc_code 377 toc_tree = generate_toc_tree(@toc, @toc_code[0], @toc_code[1] || {}) 378 text = if !toc_tree.children.empty? 379 convert(toc_tree, 0) 380 else 381 '' 382 end 383 result.sub!(/#{@toc_code.last}/, text.gsub(/\\/, "\\\\\\\\")) 384 end 385 result 386 end
# File lib/kramdown/converter/html.rb 344 def convert_smart_quote(el, _indent) 345 entity_to_str(smart_quote_entity(el)) 346 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 attr = el.attr.dup 100 figure_attr = {} 101 figure_attr['class'] = attr.delete('class') if attr.key?('class') 102 figure_attr['id'] = attr.delete('id') if attr.key?('id') 103 body = "#{' ' * (indent + @indent)}<img#{html_attributes(attr)} />\n" \ 104 "#{' ' * (indent + @indent)}<figcaption>#{attr['alt']}</figcaption>\n" 105 format_as_indented_block_html("figure", figure_attr, body, indent) 106 end
# File lib/kramdown/converter/html.rb 235 def convert_table(el, indent) 236 format_as_indented_block_html(el.type, el.attr, inner(el, indent), indent) 237 end
# File lib/kramdown/converter/html.rb 245 def convert_td(el, indent) 246 res = inner(el, indent) 247 type = (@stack[-2].type == :thead ? :th : :td) 248 attr = el.attr 249 alignment = @stack[-3].options[:alignment][@stack.last.children.index(el)] 250 if alignment != :default 251 attr = el.attr.dup 252 attr['style'] = (attr.key?('style') ? "#{attr['style']}; " : '') + "text-align: #{alignment}" 253 end 254 format_as_block_html(type, attr, res.empty? ? entity_to_str(ENTITY_NBSP) : res, indent) 255 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 336 def convert_typographic_sym(el, _indent) 337 if (result = @options[:typographic_symbols][el.value]) 338 escape_html(result, :text) 339 else 340 TYPOGRAPHIC_SYMS[el.value].map {|e| entity_to_str(e) }.join('') 341 end 342 end
# File lib/kramdown/converter/html.rb 159 def convert_ul(el, indent) 160 if !@toc_code && el.options.dig(:ial, :refs)&.include?('toc') 161 @toc_code = [el.type, el.attr, ZERO_TO_ONETWENTYEIGHT.map { rand(36).to_s(36) }.join] 162 @toc_code.last 163 elsif !@footnote_location && el.options.dig(:ial, :refs)&.include?('footnotes') 164 @footnote_location = ZERO_TO_ONETWENTYEIGHT.map { rand(36).to_s(36) }.join 165 else 166 format_as_indented_block_html(el.type, el.attr, inner(el, indent), indent) 167 end 168 end
# File lib/kramdown/converter/html.rb 225 def convert_xml_comment(el, indent) 226 if el.options[:category] == :block && 227 (@stack.last.type != :html_element || @stack.last.options[:content_model] != :raw) 228 ' ' * indent << el.value << "\n" 229 else 230 el.value 231 end 232 end
Fixes the elements for use in a TOC entry.
# File lib/kramdown/converter/html.rb 450 def fix_for_toc_entry(elements) 451 remove_footnotes(elements) 452 unwrap_links(elements) 453 elements 454 end
Return an HTML ordered list with the footnote content for the used footnotes.
# File lib/kramdown/converter/html.rb 485 def footnote_content 486 ol = Element.new(:ol) 487 ol.attr['start'] = @footnote_start if @footnote_start != 1 488 i = 0 489 backlink_text = escape_html(@options[:footnote_backlink], :text) 490 while i < @footnotes.length 491 name, data, _, repeat = *@footnotes[i] 492 li = Element.new(:li, nil, 'id' => "fn:#{name}", 'role' => 'doc-endnote') 493 li.children = Marshal.load(Marshal.dump(data.children)) 494 495 para = nil 496 if li.children.last.type == :p || @options[:footnote_backlink_inline] 497 parent = li 498 while !parent.children.empty? && ![:p, :header].include?(parent.children.last.type) 499 parent = parent.children.last 500 end 501 para = parent.children.last 502 insert_space = true 503 end 504 505 unless para 506 li.children << (para = Element.new(:p)) 507 insert_space = false 508 end 509 510 unless @options[:footnote_backlink].empty? 511 nbsp = entity_to_str(ENTITY_NBSP) 512 value = sprintf(FOOTNOTE_BACKLINK_FMT, (insert_space ? nbsp : ''), name, backlink_text) 513 para.children << Element.new(:raw, value) 514 (1..repeat).each do |index| 515 value = sprintf(FOOTNOTE_BACKLINK_FMT, nbsp, "#{name}:#{index}", 516 "#{backlink_text}<sup>#{index + 1}</sup>") 517 para.children << Element.new(:raw, value) 518 end 519 end 520 521 ol.children << Element.new(:raw, convert(li, 4)) 522 i += 1 523 end 524 if ol.children.empty? 525 '' 526 else 527 format_as_indented_block_html('div', {class: "footnotes", role: "doc-endnotes"}, convert(ol, 2), 0) 528 end 529 end
Format the given element as block HTML.
# File lib/kramdown/converter/html.rb 394 def format_as_block_html(name, attr, body, indent) 395 "#{' ' * indent}<#{name}#{html_attributes(attr)}>#{body}</#{name}>\n" 396 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 400 def format_as_indented_block_html(name, attr, body, indent) 401 "#{' ' * indent}<#{name}#{html_attributes(attr)}>\n#{body}#{' ' * indent}</#{name}>\n" 402 end
Format the given element as span HTML.
# File lib/kramdown/converter/html.rb 389 def format_as_span_html(name, attr, body) 390 "<#{name}#{html_attributes(attr)}>#{body}</#{name}>" 391 end
Generate and return an element tree for the table of contents.
# File lib/kramdown/converter/html.rb 412 def generate_toc_tree(toc, type, attr) 413 sections = Element.new(type, nil, attr.dup) 414 sections.attr['id'] ||= 'markdown-toc' 415 stack = [] 416 toc.each do |level, id, children| 417 li = Element.new(:li, nil, nil, level: level) 418 li.children << Element.new(:p, nil, nil, transparent: true) 419 a = Element.new(:a, nil) 420 a.attr['href'] = "##{id}" 421 a.attr['id'] = "#{sections.attr['id']}-#{id}" 422 a.children.concat(fix_for_toc_entry(Marshal.load(Marshal.dump(children)))) 423 li.children.last.children << a 424 li.children << Element.new(type) 425 426 success = false 427 until success 428 if stack.empty? 429 sections.children << li 430 stack << li 431 success = true 432 elsif stack.last.options[:level] < li.options[:level] 433 stack.last.children.last.children << li 434 stack << li 435 success = true 436 else 437 item = stack.pop 438 item.children.pop if item.children.last.children.empty? 439 end 440 end 441 end 442 until stack.empty? 443 item = stack.pop 444 item.children.pop if item.children.last.children.empty? 445 end 446 sections 447 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 473 def obfuscate(text) 474 result = +'' 475 text.each_byte do |b| 476 result << (b > 128 ? b.chr : sprintf("&#%03d;", b)) 477 end 478 result.force_encoding(text.encoding) 479 result 480 end
Remove all footnotes from the given elements.
# File lib/kramdown/converter/html.rb 465 def remove_footnotes(elements) 466 elements.delete_if do |c| 467 remove_footnotes(c.children) 468 c.type == :footnote 469 end 470 end
Remove all link elements by unwrapping them.
# File lib/kramdown/converter/html.rb 457 def unwrap_links(elements) 458 elements.map! do |c| 459 unwrap_links(c.children) 460 c.type == :a ? c.children : c 461 end.flatten! 462 end