Add bin and edit workflow
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 9s

This commit is contained in:
2026-09-16 13:11:16 -06:00
parent c8ac4fcae5
commit 4cee170d66
17576 changed files with 895740 additions and 2 deletions
@@ -0,0 +1,257 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2026 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
require 'erb'
require 'kramdown/utils'
require 'kramdown/document'
module Kramdown
module Converter
# == \Base class for converters
#
# This class serves as base class for all converters. It provides methods that can/should be
# used by all converters (like #generate_id) as well as common functionality that is
# automatically applied to the result (for example, embedding the output into a template).
#
# A converter object is used as a throw-away object, i.e. it is only used for storing the needed
# state information during conversion. Therefore one can't instantiate a converter object
# directly but only use the Base::convert method.
#
# == Implementing a converter
#
# Implementing a new converter is rather easy: just derive a new class from this class and put
# it in the Kramdown::Converter module (the latter is only needed if auto-detection should work
# properly). Then you need to implement the #convert method which has to contain the conversion
# code for converting an element and has to return the conversion result.
#
# The actual transformation of the document tree can be done in any way. However, writing one
# method per element type is a straight forward way to do it - this is how the Html and Latex
# converters do the transformation.
#
# Have a look at the Base::convert method for additional information!
class Base
# Can be used by a converter for storing arbitrary information during the conversion process.
attr_reader :data
# The hash with the conversion options.
attr_reader :options
# The root element that is converted.
attr_reader :root
# The warnings array.
attr_reader :warnings
# Initialize the converter with the given +root+ element and +options+ hash.
def initialize(root, options)
@options = options
@root = root
@data = {}
@warnings = []
end
private_class_method(:new, :allocate)
# Returns whether the template should be applied before the conversion of the tree.
#
# Defaults to false.
def apply_template_before?
false
end
# Returns whether the template should be applied after the conversion of the tree.
#
# Defaults to true.
def apply_template_after?
true
end
# Convert the element tree +tree+ and return the resulting conversion object (normally a
# string) and an array with warning messages. The parameter +options+ specifies the conversion
# options that should be used.
#
# Initializes a new instance of the calling class and then calls the #convert method with
# +tree+ as parameter.
#
# If the +template+ option is specified and non-empty, the template is evaluate with ERB
# before and/or after the tree conversion depending on the result of #apply_template_before?
# and #apply_template_after?. If the template is evaluated before, an empty string is used for
# the body; if evaluated after, the result is used as body. See ::apply_template.
#
# The template resolution is done in the following way (for the converter ConverterName):
#
# 1. Look in the current working directory for the template.
#
# 2. Append +.converter_name+ (e.g. +.html+) to the template name and look for the resulting
# file in the current working directory (the form +.convertername+ is deprecated).
#
# 3. Append +.converter_name+ to the template name and look for it in the kramdown data
# directory (the form +.convertername+ is deprecated).
#
# 4. Check if the template name starts with 'string://' and if so, strip this prefix away and
# use the rest as template.
def self.convert(tree, options = {})
converter = new(tree, ::Kramdown::Options.merge(options.merge(tree.options[:options] || {})))
if !converter.options[:template].empty? && converter.apply_template_before?
apply_template(converter, '')
end
result = converter.convert(tree)
if result.respond_to?(:encode!) && result.encoding != Encoding::BINARY
result.encode!(tree.options[:encoding] ||
(raise ::Kramdown::Error, "Missing encoding option on root element"))
end
if !converter.options[:template].empty? && converter.apply_template_after?
result = apply_template(converter, result)
end
[result, converter.warnings]
end
# Convert the element +el+ and return the resulting object.
#
# This is the only method that has to be implemented by sub-classes!
def convert(_el)
raise NotImplementedError
end
# Apply the +template+ using +body+ as the body string.
#
# The template is evaluated using ERB and the body is available in the @body instance variable
# and the converter object in the @converter instance variable.
def self.apply_template(converter, body) # :nodoc:
erb = ERB.new(get_template(converter.options[:template]))
obj = Object.new
obj.instance_variable_set(:@converter, converter)
obj.instance_variable_set(:@body, body)
erb.result(obj.instance_eval { binding })
end
# Return the template specified by +template+.
def self.get_template(template) # :nodoc:
format_ext = '.' + ::Kramdown::Utils.snake_case(name.split("::").last)
shipped = File.join(::Kramdown.data_dir, template + format_ext)
if File.exist?(template)
File.read(template)
elsif File.exist?(template + format_ext)
File.read(template + format_ext)
elsif File.exist?(shipped)
File.read(shipped)
elsif template.start_with?('string://')
template.delete_prefix("string://")
else
raise "The specified template file #{template} does not exist"
end
end
# Add the given warning +text+ to the warning array.
def warning(text)
@warnings << text
end
# Return +true+ if the header element +el+ should be used for the table of contents (as
# specified by the +toc_levels+ option).
def in_toc?(el)
@options[:toc_levels].include?(el.options[:level]) && (el.attr['class'] || '') !~ /\bno_toc\b/
end
# Return the output header level given a level.
#
# Uses the +header_offset+ option for adjusting the header level.
def output_header_level(level)
[[level + @options[:header_offset], 6].min, 1].max
end
# Extract the code block/span language from the attributes.
def extract_code_language(attr)
if attr['class'] && attr['class'] =~ /\blanguage-\S+/
attr['class'].scan(/\blanguage-(\S+)/).first.first
end
end
# See #extract_code_language
#
# *Warning*: This version will modify the given attributes if a language is present.
def extract_code_language!(attr)
lang = extract_code_language(attr)
attr['class'] = attr['class'].sub(/\blanguage-\S+/, '').strip if lang
attr.delete('class') if lang && attr['class'].empty?
lang
end
# Highlight the given +text+ in the language +lang+ with the syntax highlighter configured
# through the option 'syntax_highlighter'.
def highlight_code(text, lang, type, opts = {})
return nil unless @options[:syntax_highlighter]
highlighter = ::Kramdown::Converter.syntax_highlighter(@options[:syntax_highlighter])
if highlighter
highlighter.call(self, text, lang, type, opts)
else
warning("The configured syntax highlighter #{@options[:syntax_highlighter]} is not available.")
nil
end
end
# Format the given math element with the math engine configured through the option
# 'math_engine'.
def format_math(el, opts = {})
return nil unless @options[:math_engine]
engine = ::Kramdown::Converter.math_engine(@options[:math_engine])
if engine
engine.call(self, el, opts)
else
warning("The configured math engine #{@options[:math_engine]} is not available.")
nil
end
end
# Generate an unique alpha-numeric ID from the the string +str+ for use as a header ID.
#
# Uses the option +auto_id_prefix+: the value of this option is prepended to every generated
# ID.
def generate_id(str)
str = ::Kramdown::Utils::Unidecoder.decode(str) if @options[:transliterated_header_ids]
gen_id = basic_generate_id(str)
gen_id = 'section' if gen_id.empty?
@used_ids ||= {}
if @used_ids.key?(gen_id)
gen_id += "-#{@used_ids[gen_id] += 1}"
else
@used_ids[gen_id] = 0
end
@options[:auto_id_prefix] + gen_id
end
# The basic version of the ID generator, without any special provisions for empty or unique
# IDs.
def basic_generate_id(str)
gen_id = str.gsub(/^[^a-zA-Z]+/, '')
gen_id.tr!('^a-zA-Z0-9 -', '')
gen_id.tr!(' ', '-')
gen_id.downcase!
gen_id
end
SMART_QUOTE_INDICES = {lsquo: 0, rsquo: 1, ldquo: 2, rdquo: 3} # :nodoc:
# Return the entity that represents the given smart_quote element.
def smart_quote_entity(el)
res = @options[:smart_quotes][SMART_QUOTE_INDICES[el.value]]
::Kramdown::Utils::Entities.entity(res)
end
end
end
end
@@ -0,0 +1,38 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2026 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
require 'kramdown/parser'
require 'kramdown/converter'
require 'kramdown/utils'
module Kramdown
module Converter
# Converts a Kramdown::Document to a nested hash for further processing or debug output.
class HashAST < Base
def convert(el)
hash = {type: el.type}
hash[:attr] = el.attr unless el.attr.empty?
hash[:value] = el.value unless el.value.nil?
hash[:options] = el.options unless el.options.empty?
unless el.children.empty?
hash[:children] = []
el.children.each {|child| hash[:children] << convert(child) }
end
hash
end
end
HashAst = HashAST
end
end
@@ -0,0 +1,545 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2026 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
require 'kramdown/parser'
require 'kramdown/converter'
require 'kramdown/utils'
module Kramdown
module Converter
# 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.
class Html < Base
include ::Kramdown::Utils::Html
include ::Kramdown::Parser::Html::Constants
# The amount of indentation used when nesting HTML tags.
attr_accessor :indent
# Initialize the HTML converter with the given Kramdown document +doc+.
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 = []
# stash string representation of symbol to avoid allocations from multiple interpolations.
@highlighter_class = " highlighter-#{options[:syntax_highlighter]}"
@dispatcher = Hash.new {|h, k| h[k] = :"convert_#{k}" }
end
# Dispatch the conversion of the element +el+ to a +convert_TYPE+ method using the +type+ of
# the element.
def convert(el, indent = -@indent)
send(@dispatcher[el.type], el, indent)
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.
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
def convert_blank(_el, _indent)
"\n"
end
def convert_text(el, _indent)
escaped = escape_html(el.value, :text)
@options[:remove_line_breaks_for_cjk] ? fix_cjk_line_break(escaped) : escaped
end
def convert_p(el, indent)
if el.options[:transparent]
inner(el, indent)
elsif el.children.size == 1 && el.children.first.type == :img &&
el.children.first.options[:ial]&.[](:refs)&.include?('standalone')
convert_standalone_image(el, indent)
else
format_as_block_html("p", el.attr, inner(el, indent), indent)
end
end
# Helper method used by +convert_p+ to convert a paragraph that only contains a single :img
# element.
def convert_standalone_image(el, indent)
figure_attr = el.attr.dup
image_attr = el.children.first.attr.dup
if image_attr.key?('class') && !figure_attr.key?('class')
figure_attr['class'] = image_attr.delete('class')
end
if image_attr.key?('id') && !figure_attr.key?('id')
figure_attr['id'] = image_attr.delete('id')
end
body = "#{' ' * (indent + @indent)}<img#{html_attributes(image_attr)} />\n" \
"#{' ' * (indent + @indent)}<figcaption>#{image_attr['alt']}</figcaption>\n"
format_as_indented_block_html("figure", figure_attr, body, indent)
end
def convert_codeblock(el, indent)
attr = el.attr.dup
lang = extract_code_language!(attr)
hl_opts = {}
highlighted_code = highlight_code(el.value, el.options[:lang] || lang, :block, hl_opts)
if highlighted_code
add_syntax_highlighter_to_class_attr(attr, lang || hl_opts[:default_lang])
"#{' ' * 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
def convert_blockquote(el, indent)
format_as_indented_block_html("blockquote", el.attr, inner(el, indent), indent)
end
def convert_header(el, indent)
attr = el.attr.dup
if @options[:auto_ids] && !attr['id']
attr['id'] = generate_id(el.options[:raw_text])
end
if @options[:header_links] && attr['id'].to_s.length > 0
link = Element.new(:a, nil, nil)
link.attr['href'] = "##{attr['id']}"
el.children.unshift(link)
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
def convert_hr(el, indent)
"#{' ' * indent}<hr#{html_attributes(el.attr)} />\n"
end
ZERO_TO_ONETWENTYEIGHT = (0..128).to_a.freeze
private_constant :ZERO_TO_ONETWENTYEIGHT
def convert_ul(el, indent)
if !@toc_code && el.options.dig(:ial, :refs)&.include?('toc')
@toc_code = [el.type, el.attr, ZERO_TO_ONETWENTYEIGHT.map { rand(36).to_s(36) }.join]
@toc_code.last
elsif !@footnote_location && el.options.dig(:ial, :refs)&.include?('footnotes')
@footnote_location = ZERO_TO_ONETWENTYEIGHT.map { rand(36).to_s(36) }.join
else
format_as_indented_block_html(el.type, el.attr, inner(el, indent), indent)
end
end
alias convert_ol convert_ul
def convert_dl(el, indent)
format_as_indented_block_html("dl", el.attr, inner(el, indent), indent)
end
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.match?(/\n\Z/) ? ' ' * indent : '')
else
output << "\n" << res << ' ' * indent
end
output << "</#{el.type}>\n"
end
alias convert_dd convert_li
def convert_dt(el, indent)
attr = el.attr.dup
@stack.last.options[:ial][:refs].each do |ref|
if ref =~ /\Aauto_ids(?:-([\w-]+))?/
attr['id'] = "#{$1}#{basic_generate_id(el.options[:raw_text])}".lstrip
break
end
end if !attr['id'] && @stack.last.options[:ial] && @stack.last.options[:ial][:refs]
format_as_block_html("dt", attr, inner(el, indent), indent)
end
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 = +''
if @stack.last.type != :html_element || @stack.last.options[:content_model] != :raw
output << ' ' * indent
end
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
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
alias convert_xml_pi convert_xml_comment
def convert_table(el, indent)
format_as_indented_block_html(el.type, el.attr, inner(el, indent), indent)
end
alias convert_thead convert_table
alias convert_tbody convert_table
alias convert_tfoot convert_table
alias convert_tr convert_table
ENTITY_NBSP = ::Kramdown::Utils::Entities.entity('nbsp') # :nodoc:
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.key?('style') ? "#{attr['style']}; " : '') + "text-align: #{alignment}"
end
format_as_block_html(type, attr, res.empty? ? entity_to_str(ENTITY_NBSP) : res, indent)
end
def convert_comment(el, indent)
if el.options[:category] == :block
"#{' ' * indent}<!-- #{el.value} -->\n"
else
"<!-- #{el.value} -->"
end
end
def convert_br(_el, _indent)
"<br />"
end
def convert_a(el, indent)
format_as_span_html("a", el.attr, inner(el, indent))
end
def convert_img(el, _indent)
"<img#{html_attributes(el.attr)} />"
end
def convert_codespan(el, _indent)
attr = el.attr.dup
lang = extract_code_language(attr)
hl_opts = {}
result = highlight_code(el.value, lang, :span, hl_opts)
if result
add_syntax_highlighter_to_class_attr(attr, lang || hl_opts[:default_lang])
else
result = escape_html(el.value)
end
format_as_span_html('code', attr, result)
end
def convert_footnote(el, _indent)
repeat = ''
name = @options[:footnote_prefix] + el.options[:name]
if (footnote = @footnotes_by_name[name])
number = footnote[2]
repeat = ":#{footnote[3] += 1}"
else
number = @footnote_counter
@footnote_counter += 1
@footnotes << [name, el.value, number, 0]
@footnotes_by_name[name] = @footnotes.last
end
formatted_link_text = sprintf(@options[:footnote_link_text], number)
"<sup id=\"fnref:#{name}#{repeat}\">" \
"<a href=\"#fn:#{name}\" class=\"footnote\" rel=\"footnote\" role=\"doc-noteref\">" \
"#{formatted_link_text}</a></sup>"
end
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
def convert_em(el, indent)
format_as_span_html(el.type, el.attr, inner(el, indent))
end
alias convert_strong convert_em
def convert_entity(el, _indent)
entity_to_str(el.value, el.options[:original])
end
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')],
} # :nodoc:
def convert_typographic_sym(el, _indent)
if (result = @options[:typographic_symbols][el.value])
escape_html(result, :text)
else
TYPOGRAPHIC_SYMS[el.value].map {|e| entity_to_str(e) }.join
end
end
def convert_smart_quote(el, _indent)
entity_to_str(smart_quote_entity(el))
end
def convert_math(el, indent)
if (result = format_math(el, indent: indent))
result
else
attr = el.attr.dup
attr['class'] = "#{attr['class']} kdmath".lstrip
if el.options[:category] == :block
format_as_block_html('div', attr, "$$\n#{el.value}\n$$", indent)
else
format_as_span_html('span', attr, "$#{el.value}$")
end
end
end
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
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 = toc_tree.children.empty? ? '' : convert(toc_tree, 0)
result.sub!(/#{@toc_code.last}/, text.gsub(/\\/, "\\\\\\\\"))
end
result
end
# Format the given element as span HTML.
def format_as_span_html(name, attr, body)
"<#{name}#{html_attributes(attr)}>#{body}</#{name}>"
end
# Format the given element as block HTML.
def format_as_block_html(name, attr, body, indent)
"#{' ' * indent}<#{name}#{html_attributes(attr)}>#{body}</#{name}>\n"
end
# Format the given element as block HTML with a newline after the start tag and indentation
# before the end tag.
def format_as_indented_block_html(name, attr, body, indent)
"#{' ' * indent}<#{name}#{html_attributes(attr)}>\n#{body}#{' ' * indent}</#{name}>\n"
end
# 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.
def add_syntax_highlighter_to_class_attr(attr, lang = nil)
(attr['class'] = (attr['class'] || '') + @highlighter_class).lstrip!
attr['class'].sub!(/\blanguage-\S+|(^)/) { "language-#{lang}#{$1 ? ' ' : ''}" } if lang
end
# Generate and return an element tree for the table of contents.
def generate_toc_tree(toc, type, attr)
sections = Element.new(type, nil, attr.dup)
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(fix_for_toc_entry(Marshal.load(Marshal.dump(children))))
li.children.last.children << a
li.children << Element.new(type)
success = false
until 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 if item.children.last.children.empty?
end
end
end
until stack.empty?
item = stack.pop
item.children.pop if item.children.last.children.empty?
end
sections
end
# Fixes the elements for use in a TOC entry.
def fix_for_toc_entry(elements)
remove_footnotes(elements)
unwrap_links(elements)
elements
end
# Remove all link elements by unwrapping them.
def unwrap_links(elements)
elements.map! do |c|
unwrap_links(c.children)
c.type == :a ? c.children : c
end.flatten!
end
# Remove all footnotes from the given elements.
def remove_footnotes(elements)
elements.delete_if do |c|
remove_footnotes(c.children)
c.type == :footnote
end
end
# Obfuscate the +text+ by using HTML entities.
def obfuscate(text)
result = +''
text.each_byte do |b|
result << (b > 128 ? b.chr : sprintf("&#%03d;", b))
end
result.force_encoding(text.encoding)
result
end
FOOTNOTE_BACKLINK_FMT = "%s<a href=\"#fnref:%s\" class=\"reversefootnote\" role=\"doc-backlink\">%s</a>"
# Return an HTML ordered list with the footnote content for the used footnotes.
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))
para = nil
if li.children.last.type == :p || @options[:footnote_backlink_inline]
parent = li
while !parent.children.empty? && ![:p, :header].include?(parent.children.last.type)
parent = parent.children.last
end
para = parent.children.last
insert_space = true
end
unless para
li.children << (para = Element.new(:p))
insert_space = false
end
unless @options[:footnote_backlink].empty?
nbsp = entity_to_str(ENTITY_NBSP)
value = sprintf(FOOTNOTE_BACKLINK_FMT, (insert_space ? nbsp : ''), name, backlink_text)
para.children << Element.new(:raw, value)
(1..repeat).each do |index|
value = sprintf(FOOTNOTE_BACKLINK_FMT, nbsp, "#{name}:#{index}",
"#{backlink_text}<sup>#{index + 1}</sup>")
para.children << Element.new(:raw, value)
end
end
ol.children << Element.new(:raw, convert(li, 4))
i += 1
end
if ol.children.empty?
''
else
format_as_indented_block_html('div', {class: "footnotes", role: "doc-endnotes"}, convert(ol, 2), 0)
end
end
end
end
end
@@ -0,0 +1,458 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2026 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
require 'kramdown/converter'
require 'kramdown/utils'
module Kramdown
module Converter
# Converts an element tree to the kramdown format.
class Kramdown < Base
# :stopdoc:
include ::Kramdown::Utils::Html
def initialize(root, options)
super
@linkrefs = []
@footnotes = []
@abbrevs = []
@stack = []
@list_indent = @options[:list_indent]
@list_spacing = ' ' * (@list_indent - 2)
end
def convert(el, opts = {indent: 0})
res = send("convert_#{el.type}", el, opts)
res = res.dup if res.frozen?
if ![:html_element, :li, :dt, :dd, :td].include?(el.type) && (ial = ial_for_element(el))
res << ial
res << "\n\n" if el.block?
elsif [:ul, :dl, :ol, :codeblock].include?(el.type) && opts[:next] &&
([el.type, :codeblock].include?(opts[:next].type) ||
(opts[:next].type == :blank && opts[:nnext] &&
[el.type, :codeblock].include?(opts[:nnext].type)))
res << "^\n\n"
elsif el.block? &&
![:li, :dd, :dt, :td, :th, :tr, :thead, :tbody, :tfoot, :blank].include?(el.type) &&
(el.type != :html_element || @stack.last.type != :html_element) &&
(el.type != :p || !el.options[:transparent]) &&
!([:ul, :dl, :ol].include?(el.type) && @stack.last.type == :li)
res << "\n"
end
res
end
def inner(el, opts = {indent: 0})
@stack.push(el)
result = +''
el.children.each_with_index do |inner_el, index|
options = opts.dup
options[:index] = index
options[:prev] = (index == 0 ? nil : el.children[index - 1])
options[:pprev] = (index <= 1 ? nil : el.children[index - 2])
options[:next] = (index == el.children.length - 1 ? nil : el.children[index + 1])
options[:nnext] = (index >= el.children.length - 2 ? nil : el.children[index + 2])
result << convert(inner_el, options)
end
@stack.pop
result
end
def convert_blank(_el, _opts)
""
end
ESCAPED_CHAR_RE = /(\$\$|[\\*_`\[\]{"'|])|^ {0,3}(:)/
def convert_text(el, opts)
if opts[:raw_text] || (@stack.last.type == :html_element && @stack.last.options[:content_model] == :raw)
el.value
else
result = el.value.gsub(/\A\n/) do
opts[:prev] && opts[:prev].type == :br ? '' : "\n"
end
result.gsub!(/\s+/, ' ') unless el.options[:cdata]
result.gsub!(ESCAPED_CHAR_RE) do
$1 || !opts[:prev] || opts[:prev].type == :br ? "\\#{$1 || $2}" : $&
end
result
end
end
def convert_p(el, opts)
w = @options[:line_width] - opts[:indent].to_s.to_i
first, second, *rest = inner(el, opts).strip.gsub(/(.{1,#{w}})( +|$\n?)/, "\\1\n").split(/\n/)
first&.gsub!(/^(?:(#|>)|(\d+)\.(\s)|([+-]\s))/) { $1 || $4 ? "\\#{$1 || $4}" : "#{$2}\\.#{$3}" }
second&.gsub!(/^([=-]+\s*?)$/, "\\\1")
res = [first, second, *rest].compact.join("\n") + "\n"
res.gsub!(/^ {0,3}:/, "\\:")
if el.children.length == 1 && el.children.first.type == :math
res = "\\#{res}"
elsif res.start_with?('\$$') && res.end_with?("\\$$\n")
res.sub!(/^\\\$\$/, '\$\$')
end
res
end
def convert_codeblock(el, _opts)
el.value.split("\n").map {|l| l.empty? ? " " : " #{l}" }.join("\n") + "\n"
end
def convert_blockquote(el, opts)
opts[:indent] += 2
inner(el, opts).chomp.split("\n").map {|l| "> #{l}" }.join("\n") << "\n"
end
def convert_header(el, opts)
opts[:in_header] = true
res = +''
res << "#{'#' * output_header_level(el.options[:level])} #{inner(el, opts)}"
res[-1, 1] = "\\#" if res[-1] == '#'
res << " {##{el.attr['id']}}" if el.attr['id'] && !el.attr['id'].strip.empty?
res << "\n"
end
def convert_hr(_el, _opts)
"* * *\n"
end
def convert_ul(el, opts)
inner(el, opts).sub(/\n+\Z/, "\n")
end
alias convert_ol convert_ul
alias convert_dl convert_ul
def convert_li(el, opts)
sym, width = if @stack.last.type == :ul
['* ' + @list_spacing, el.children.first && el.children.first.type == :codeblock ? 4 : @list_indent]
else
["#{opts[:index] + 1}.".ljust(4), 4]
end
if (ial = ial_for_element(el))
sym << ial << " "
end
opts[:indent] += width
text = inner(el, opts)
newlines = text.scan(/\n*\Z/).first
first, *last = text.split("\n")
last = last.map {|l| " " * width + l }.join("\n")
text = (first.nil? ? "\n" : first + (last.empty? ? "" : "\n") + last + newlines)
if el.children.first && el.children.first.type == :p && !el.children.first.options[:transparent]
res = +"#{sym}#{text}"
res << "^\n" if el.children.size == 1 && @stack.last.children.last == el &&
(@stack.last.children.any? {|c| !c.children.first || c.children.first.type != :p } || @stack.last.children.size == 1)
res
elsif el.children.first && el.children.first.type == :codeblock
"#{sym}\n #{text}"
else
"#{sym}#{text}"
end
end
def convert_dd(el, opts)
sym, width = ": " + @list_spacing, (el.children.first && el.children.first.type == :codeblock ? 4 : @list_indent)
if (ial = ial_for_element(el))
sym << ial << " "
end
opts[:indent] += width
text = inner(el, opts)
newlines = text.scan(/\n*\Z/).first
first, *last = text.split("\n")
last = last.map {|l| " " * width + l }.join("\n")
text = first.to_s + (last.empty? ? "" : "\n") + last + newlines
text.chomp! if text =~ /\n\n\Z/ && opts[:next] && opts[:next].type == :dd
text << "\n" if text !~ /\n\n\Z/ && opts[:next] && opts[:next].type == :dt
text << "\n" if el.children.empty?
if el.children.first && el.children.first.type == :p && !el.children.first.options[:transparent]
"\n#{sym}#{text}"
elsif el.children.first && el.children.first.type == :codeblock
"#{sym}\n #{text}"
else
"#{sym}#{text}"
end
end
def convert_dt(el, opts)
result = +''
if (ial = ial_for_element(el))
result << ial << " "
end
result << inner(el, opts) << "\n"
end
HTML_TAGS_WITH_BODY = %w[div script iframe textarea th td]
HTML_ELEMENT_TYPES = [:entity, :text, :html_element].freeze
private_constant :HTML_ELEMENT_TYPES
def convert_html_element(el, opts)
markdown_attr = el.options[:category] == :block && el.children.any? do |c|
c.type != :html_element &&
(c.type != :p || !c.options[:transparent] ||
c.children.any? {|t| !HTML_ELEMENT_TYPES.member?(t.type) }) &&
c.block?
end
opts[:force_raw_text] = true if %w[script pre code].include?(el.value)
opts[:raw_text] = opts[:force_raw_text] || opts[:block_raw_text] || \
(el.options[:category] != :span && !markdown_attr)
opts[:block_raw_text] = true if el.options[:category] == :block && opts[:raw_text]
res = inner(el, opts)
if el.options[:category] == :span
"<#{el.value}#{html_attributes(el.attr)}" + \
(!res.empty? || HTML_TAGS_WITH_BODY.include?(el.value) ? ">#{res}</#{el.value}>" : " />")
else
output = +''
attr = el.attr.dup
attr['markdown'] = '1' if markdown_attr
output << "<#{el.value}#{html_attributes(attr)}"
if !res.empty? && el.options[:content_model] != :block
output << ">#{res}</#{el.value}>"
elsif !res.empty?
output << ">\n#{res}" << "</#{el.value}>"
elsif HTML_TAGS_WITH_BODY.include?(el.value)
output << "></#{el.value}>"
else
output << " />"
end
output << "\n" if @stack.last.type != :html_element || @stack.last.options[:content_model] != :raw
output
end
end
def convert_xml_comment(el, _opts)
if el.options[:category] == :block &&
(@stack.last.type != :html_element || @stack.last.options[:content_model] != :raw)
el.value + "\n"
else
el.value.dup
end
end
alias convert_xml_pi convert_xml_comment
def convert_table(el, opts)
opts[:alignment] = el.options[:alignment]
inner(el, opts)
end
def convert_thead(el, opts)
rows = inner(el, opts)
if opts[:alignment].all?(:default)
"#{rows}|#{'-' * 10}\n"
else
"#{rows}| " + opts[:alignment].map do |a|
case a
when :left then ":-"
when :right then "-:"
when :center then ":-:"
when :default then "-"
end
end.join(' ') << "\n"
end
end
def convert_tbody(el, opts)
res = +''
res << inner(el, opts)
res << '|' << '-' * 10 << "\n" if opts[:next] && opts[:next].type == :tbody
res
end
def convert_tfoot(el, opts)
"|#{'=' * 10}\n#{inner(el, opts)}"
end
def convert_tr(el, opts)
"| #{el.children.map {|c| convert(c, opts) }.join(' | ')} |\n"
end
def convert_td(el, opts)
inner(el, opts)
end
def convert_comment(el, _opts)
if el.options[:category] == :block
"{::comment}\n#{el.value}\n{:/}\n"
else
"{::comment}#{el.value}{:/}"
end
end
def convert_br(_el, opts)
opts[:in_header] ? "<br />" : " \n"
end
def convert_a(el, opts)
if el.attr['href'].empty?
"[#{inner(el, opts)}]()"
elsif el.attr['href'] =~ /^(?:http|ftp)/ || el.attr['href'].count("()") > 0
index = if (link_el = @linkrefs.find {|c| c.attr['href'] == el.attr['href'] })
@linkrefs.index(link_el) + 1
else
@linkrefs << el
@linkrefs.size
end
"[#{inner(el, opts)}][#{index}]"
else
title = parse_title(el.attr['title'])
"[#{inner(el, opts)}](#{el.attr['href']}#{title})"
end
end
def convert_img(el, _opts)
alt_text = el.attr['alt'].to_s.gsub(ESCAPED_CHAR_RE) { $1 ? "\\#{$1}" : $2 }
src = el.attr['src'].to_s
if src.empty?
"![#{alt_text}]()"
else
title = parse_title(el.attr['title'])
link = if src.count("()") > 0
"<#{src}>"
else
src
end
"![#{alt_text}](#{link}#{title})"
end
end
def convert_codespan(el, _opts)
delim = (el.value.scan(/`+/).max || '') + '`'
"#{delim}#{' ' if delim.size > 1}#{el.value}#{' ' if delim.size > 1}#{delim}"
end
def convert_footnote(el, _opts)
@footnotes << [el.options[:name], el.value]
"[^#{el.options[:name]}]"
end
def convert_raw(el, _opts)
attr = (el.options[:type] || []).join(' ')
attr = " type=\"#{attr}\"" unless attr.empty?
if @stack.last.type == :html_element
el.value
elsif el.options[:category] == :block
"{::nomarkdown#{attr}}\n#{el.value}\n{:/}\n"
else
"{::nomarkdown#{attr}}#{el.value}{:/}"
end
end
def convert_em(el, opts)
"*#{inner(el, opts)}*" +
(opts[:next] && [:em, :strong].include?(opts[:next].type) && !ial_for_element(el) ? '{::}' : '')
end
def convert_strong(el, opts)
"**#{inner(el, opts)}**" +
(opts[:next] && [:em, :strong].include?(opts[:next].type) && !ial_for_element(el) ? '{::}' : '')
end
def convert_entity(el, _opts)
entity_to_str(el.value, el.options[:original])
end
TYPOGRAPHIC_SYMS = {
mdash: '---', ndash: '--', hellip: '...',
laquo_space: '<< ', raquo_space: ' >>',
laquo: '<<', raquo: '>>'
}
def convert_typographic_sym(el, _opts)
TYPOGRAPHIC_SYMS[el.value]
end
def convert_smart_quote(el, _opts)
el.value.to_s.match?(/[rl]dquo/) ? "\"" : "'"
end
def convert_math(el, _opts)
"$$#{el.value}$$" + (el.options[:category] == :block ? "\n" : '')
end
def convert_abbreviation(el, _opts)
el.value
end
def convert_root(el, opts)
res = inner(el, opts)
res << create_link_defs
res << create_footnote_defs
res << create_abbrev_defs
res
end
def create_link_defs
res = +''
res << "\n\n" unless @linkrefs.empty?
@linkrefs.each_with_index do |el, i|
title = parse_title(el.attr['title'])
res << "[#{i + 1}]: #{el.attr['href']}#{title}\n"
end
res
end
def create_footnote_defs
res = +''
@footnotes.each do |name, data|
res << "[^#{name}]:\n"
res << inner(data).chomp.split("\n").map {|l| " #{l}" }.join("\n") + "\n\n"
end
res
end
def create_abbrev_defs
return '' unless @root.options[:abbrev_defs]
res = +''
@root.options[:abbrev_defs].each do |name, text|
res << "*[#{name}]: #{text}\n"
res << ial_for_element(Element.new(:unused, nil, @root.options[:abbrev_attr][name])).to_s << "\n\n"
end
res
end
# Return the IAL containing the attributes of the element +el+.
def ial_for_element(el)
res = el.attr.map do |k, v|
next if [:img, :a].include?(el.type) && ['href', 'src', 'alt', 'title'].include?(k)
next if el.type == :header && k == 'id' && !v.strip.empty?
if v.nil?
''
elsif k == 'class' && !v.empty? && !v.index(/[.#]/)
" " + v.split(/\s+/).map {|w| ".#{w}" }.join(" ")
elsif k == 'id' && !v.strip.empty?
" ##{v}"
else
" #{k}=\"#{v}\""
end
end.compact.join
res = "toc" + (res.strip.empty? ? '' : " #{res}") if (el.type == :ul || el.type == :ol) &&
el.options.dig(:ial, :refs)&.include?('toc')
res = "footnotes" + (res.strip.empty? ? '' : " #{res}") if (el.type == :ul || el.type == :ol) &&
el.options.dig(:ial, :refs)&.include?('footnotes')
if el.type == :dl && el.options[:ial] && el.options[:ial][:refs]
auto_ids = el.options[:ial][:refs].select {|ref| ref.start_with?('auto_ids') }.join(" ")
res = auto_ids << (res.strip.empty? ? '' : " #{res}") unless auto_ids.empty?
end
res.strip.empty? ? nil : "{:#{res}}"
end
def parse_title(attr)
attr.to_s.empty? ? '' : ' "' + attr.gsub(/"/, '&quot;') + '"'
end
# :startdoc:
end
end
end
@@ -0,0 +1,626 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2026 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
require 'set'
require 'kramdown/converter'
module Kramdown
module Converter
# Converts an element tree to LaTeX.
#
# This converter uses ideas from other Markdown-to-LaTeX converters like Pandoc and Maruku.
#
# You can customize this 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.
#
# [+opts+] A hash containing processing options that are passed down from parent elements. The
# key :parent is always set and contains the parent element as value.
#
# The return value of such a method has to be a string containing the element +el+ formatted
# correctly as LaTeX markup.
class Latex < Base
# Initialize the LaTeX converter with the +root+ element and the conversion +options+.
def initialize(root, options)
super
@data[:packages] = Set.new
end
# Dispatch the conversion of the element +el+ to a +convert_TYPE+ method using the +type+ of
# the element.
def convert(el, opts = {})
send("convert_#{el.type}", el, opts)
end
# Return the converted content of the children of +el+ as a string.
def inner(el, opts)
result = +''
options = opts.dup.merge(parent: el)
el.children.each_with_index do |inner_el, index|
options[:index] = index
options[:result] = result
result << send("convert_#{inner_el.type}", inner_el, options)
end
result
end
def convert_root(el, opts)
inner(el, opts)
end
def convert_blank(_el, opts)
opts[:result].match?(/\n\n\Z|\A\Z/) ? "" : "\n"
end
def convert_text(el, _opts)
escape(el.value)
end
def convert_p(el, opts)
if el.children.size == 1 && el.children.first.type == :img &&
!(img = convert_img(el.children.first, opts)).empty?
convert_standalone_image(el, opts, img)
else
"#{latex_link_target(el)}#{inner(el, opts)}\n\n"
end
end
# Helper method used by +convert_p+ to convert a paragraph that only contains a single :img
# element.
def convert_standalone_image(el, _opts, img)
attrs = attribute_list(el)
"\\begin{figure}#{attrs}\n\\begin{center}\n#{img}\n\\end{center}\n" \
"\\caption{#{escape(el.children.first.attr['alt'])}}\n" \
"#{latex_link_target(el, true)}\n\\end{figure}#{attrs}\n"
end
def convert_codeblock(el, _opts)
show_whitespace = el.attr['class'].to_s =~ /\bshow-whitespaces\b/
lang = extract_code_language(el.attr)
if @options[:syntax_highlighter] == :minted &&
(highlighted_code = highlight_code(el.value, lang, :block))
@data[:packages] << 'minted'
"#{latex_link_target(el)}#{highlighted_code}\n"
elsif show_whitespace || lang
options = []
options << (show_whitespace ? "showspaces=true,showtabs=true" : "showspaces=false,showtabs=false")
options << "language=#{lang}" if lang
options << "basicstyle=\\ttfamily\\footnotesize,columns=fixed,frame=tlbr"
id = el.attr['id']
options << "label=#{id}" if id
attrs = attribute_list(el)
"#{latex_link_target(el)}\\begin{lstlisting}[#{options.join(',')}]\n" \
"#{el.value}\n\\end{lstlisting}#{attrs}\n"
else
"#{latex_link_target(el)}\\begin{verbatim}#{el.value}\\end{verbatim}\n"
end
end
def convert_blockquote(el, opts)
latex_environment(el.children.size > 1 ? 'quotation' : 'quote', el, inner(el, opts))
end
def convert_header(el, opts)
type = @options[:latex_headers][output_header_level(el.options[:level]) - 1]
if ((id = el.attr['id']) ||
(@options[:auto_ids] && (id = generate_id(el.options[:raw_text])))) && in_toc?(el)
"\\#{type}{#{inner(el, opts)}}\\hypertarget{#{id}}{}\\label{#{id}}\n\n"
else
"\\#{type}*{#{inner(el, opts)}}\n\n"
end
end
def convert_hr(el, _opts)
attrs = attribute_list(el)
"#{latex_link_target(el)}\\begin{center}#{attrs}\n\\rule{3in}{0.4pt}\n\\end{center}#{attrs}\n"
end
def convert_ul(el, opts)
if !@data[:has_toc] && el.options.dig(:ial, :refs)&.include?('toc')
@data[:has_toc] = true
'\tableofcontents'
else
latex_environment(el.type == :ul ? 'itemize' : 'enumerate', el, inner(el, opts))
end
end
alias convert_ol convert_ul
def convert_dl(el, opts)
latex_environment('description', el, inner(el, opts))
end
def convert_li(el, opts)
"\\item{} #{latex_link_target(el, true)}#{inner(el, opts).sub(/\n+\Z/, '')}\n"
end
def convert_dt(el, opts)
"\\item[#{inner(el, opts)}] "
end
def convert_dd(el, opts)
"#{latex_link_target(el)}#{inner(el, opts)}\n\n"
end
def convert_html_element(el, opts)
case el.value
when 'i', 'em'
"\\emph{#{inner(el, opts)}}"
when 'b', 'strong'
"\\textbf{#{inner(el, opts)}}"
else
warning("Can't convert HTML element")
''
end
end
def convert_xml_comment(el, _opts)
el.value.split("\n").map {|l| "% #{l}" }.join("\n") + "\n"
end
def convert_xml_pi(_el, _opts)
warning("Can't convert XML PI")
''
end
TABLE_ALIGNMENT_CHAR = {default: 'l', left: 'l', center: 'c', right: 'r'} # :nodoc:
def convert_table(el, opts)
@data[:packages] << 'longtable'
align = el.options[:alignment].map {|a| TABLE_ALIGNMENT_CHAR[a] }.join('|')
attrs = attribute_list(el)
"#{latex_link_target(el)}\\begin{longtable}{|#{align}|}#{attrs}\n" \
"\\hline\n#{inner(el, opts)}\\hline\n\\end{longtable}#{attrs}\n\n"
end
def convert_thead(el, opts)
"#{inner(el, opts)}\\hline\n"
end
def convert_tbody(el, opts)
inner(el, opts)
end
def convert_tfoot(el, opts)
"\\hline \\hline \n#{inner(el, opts)}"
end
def convert_tr(el, opts)
el.children.map {|c| send("convert_#{c.type}", c, opts) }.join(' & ') << "\\\\\n"
end
def convert_td(el, opts)
inner(el, opts)
end
def convert_comment(el, _opts)
el.value.split("\n").map {|l| "% #{l}" }.join("\n") << "\n"
end
def convert_br(_el, opts)
res = +"\\newline"
res << "\n" if (c = opts[:parent].children[opts[:index] + 1]) &&
(c.type != :text || c.value !~ /^\s*\n/)
res
end
def convert_a(el, opts)
url = el.attr['href']
if url.start_with?('#')
"\\hyperlink{#{url[1..-1].gsub('%', '\\%')}}{#{inner(el, opts)}}"
else
"\\href{#{url.gsub('%', '\\%')}}{#{inner(el, opts)}}"
end
end
def convert_img(el, _opts)
line = el.options[:location]
if el.attr['src'].match?(/^(https?|ftps?):\/\//)
warning("Cannot include non-local image#{line ? " (line #{line})" : ''}")
''
elsif !el.attr['src'].empty?
@data[:packages] << 'graphicx'
"#{latex_link_target(el)}\\includegraphics{#{el.attr['src']}}"
else
warning("Cannot include image with empty path#{line ? " (line #{line})" : ''}")
''
end
end
def convert_codespan(el, _opts)
lang = extract_code_language(el.attr)
if @options[:syntax_highlighter] == :minted &&
(highlighted_code = highlight_code(el.value, lang, :span))
@data[:packages] << 'minted'
"#{latex_link_target(el)}#{highlighted_code}"
else
"\\texttt{#{latex_link_target(el)}#{escape(el.value)}}"
end
end
def convert_footnote(el, opts)
@data[:packages] << 'fancyvrb'
"\\footnote{#{inner(el.value, opts).rstrip}}"
end
def convert_raw(el, _opts)
if !el.options[:type] || el.options[:type].empty? || el.options[:type].include?('latex')
el.value + (el.options[:category] == :block ? "\n" : '')
else
''
end
end
def convert_em(el, opts)
"\\emph{#{latex_link_target(el)}#{inner(el, opts)}}"
end
def convert_strong(el, opts)
"\\textbf{#{latex_link_target(el)}#{inner(el, opts)}}"
end
# Inspired by Maruku: entity conversion table based on the one from htmltolatex
# (http://sourceforge.net/projects/htmltolatex/), with some small adjustments/additions
ENTITY_CONV_TABLE = {
913 => ['$A$'],
914 => ['$B$'],
915 => ['$\Gamma$'],
916 => ['$\Delta$'],
917 => ['$E$'],
918 => ['$Z$'],
919 => ['$H$'],
920 => ['$\Theta$'],
921 => ['$I$'],
922 => ['$K$'],
923 => ['$\Lambda$'],
924 => ['$M$'],
925 => ['$N$'],
926 => ['$\Xi$'],
927 => ['$O$'],
928 => ['$\Pi$'],
929 => ['$P$'],
931 => ['$\Sigma$'],
932 => ['$T$'],
933 => ['$Y$'],
934 => ['$\Phi$'],
935 => ['$X$'],
936 => ['$\Psi$'],
937 => ['$\Omega$'],
945 => ['$\alpha$'],
946 => ['$\beta$'],
947 => ['$\gamma$'],
948 => ['$\delta$'],
949 => ['$\epsilon$'],
950 => ['$\zeta$'],
951 => ['$\eta$'],
952 => ['$\theta$'],
953 => ['$\iota$'],
954 => ['$\kappa$'],
955 => ['$\lambda$'],
956 => ['$\mu$'],
957 => ['$\nu$'],
958 => ['$\xi$'],
959 => ['$o$'],
960 => ['$\pi$'],
961 => ['$\rho$'],
963 => ['$\sigma$'],
964 => ['$\tau$'],
965 => ['$\upsilon$'],
966 => ['$\phi$'],
967 => ['$\chi$'],
968 => ['$\psi$'],
969 => ['$\omega$'],
962 => ['$\varsigma$'],
977 => ['$\vartheta$'],
982 => ['$\varpi$'],
8230 => ['\ldots'],
8242 => ['$\prime$'],
8254 => ['-'],
8260 => ['/'],
8472 => ['$\wp$'],
8465 => ['$\Im$'],
8476 => ['$\Re$'],
8501 => ['$\aleph$'],
8226 => ['$\bullet$'],
8482 => ['$^{\rm TM}$'],
8592 => ['$\leftarrow$'],
8594 => ['$\rightarrow$'],
8593 => ['$\uparrow$'],
8595 => ['$\downarrow$'],
8596 => ['$\leftrightarrow$'],
8629 => ['$\hookleftarrow$'],
8657 => ['$\Uparrow$'],
8659 => ['$\Downarrow$'],
8656 => ['$\Leftarrow$'],
8658 => ['$\Rightarrow$'],
8660 => ['$\Leftrightarrow$'],
8704 => ['$\forall$'],
8706 => ['$\partial$'],
8707 => ['$\exists$'],
8709 => ['$\emptyset$'],
8711 => ['$\nabla$'],
8712 => ['$\in$'],
8715 => ['$\ni$'],
8713 => ['$\notin$'],
8721 => ['$\sum$'],
8719 => ['$\prod$'],
8722 => ['$-$'],
8727 => ['$\ast$'],
8730 => ['$\surd$'],
8733 => ['$\propto$'],
8734 => ['$\infty$'],
8736 => ['$\angle$'],
8743 => ['$\wedge$'],
8744 => ['$\vee$'],
8745 => ['$\cap$'],
8746 => ['$\cup$'],
8747 => ['$\int$'],
8756 => ['$\therefore$', 'amssymb'],
8764 => ['$\sim$'],
8776 => ['$\approx$'],
8773 => ['$\cong$'],
8800 => ['$\neq$'],
8801 => ['$\equiv$'],
8804 => ['$\leq$'],
8805 => ['$\geq$'],
8834 => ['$\subset$'],
8835 => ['$\supset$'],
8838 => ['$\subseteq$'],
8839 => ['$\supseteq$'],
8836 => ['$\nsubset$', 'amssymb'],
8853 => ['$\oplus$'],
8855 => ['$\otimes$'],
8869 => ['$\perp$'],
8901 => ['$\cdot$'],
8968 => ['$\rceil$'],
8969 => ['$\lceil$'],
8970 => ['$\lfloor$'],
8971 => ['$\rfloor$'],
9001 => ['$\rangle$'],
9002 => ['$\langle$'],
9674 => ['$\lozenge$', 'amssymb'],
9824 => ['$\spadesuit$'],
9827 => ['$\clubsuit$'],
9829 => ['$\heartsuit$'],
9830 => ['$\diamondsuit$'],
38 => ['\&'],
34 => ['"'],
39 => ['\''],
169 => ['\copyright'],
60 => ['\textless'],
62 => ['\textgreater'],
338 => ['\OE'],
339 => ['\oe'],
352 => ['\v{S}'],
353 => ['\v{s}'],
376 => ['\"Y'],
710 => ['\textasciicircum'],
732 => ['\textasciitilde'],
8211 => ['--'],
8212 => ['---'],
8216 => ['`'],
8217 => ['\''],
8220 => ['``'],
8221 => ['\'\''],
8224 => ['\dag'],
8225 => ['\ddag'],
8240 => ['\permil', 'wasysym'],
8364 => ['\euro', 'eurosym'],
8249 => ['\guilsinglleft'],
8250 => ['\guilsinglright'],
8218 => ['\quotesinglbase', 'mathcomp'],
8222 => ['\quotedblbase', 'mathcomp'],
402 => ['\textflorin', 'mathcomp'],
381 => ['\v{Z}'],
382 => ['\v{z}'],
160 => ['~'],
161 => ['\textexclamdown'],
163 => ['\pounds'],
164 => ['\currency', 'wasysym'],
165 => ['\textyen', 'textcomp'],
166 => ['\brokenvert', 'wasysym'],
167 => ['\S'],
171 => ['\guillemotleft'],
187 => ['\guillemotright'],
174 => ['\textregistered'],
170 => ['\textordfeminine'],
172 => ['$\neg$'],
173 => ['\-'],
176 => ['$\degree$', 'mathabx'],
177 => ['$\pm$'],
180 => ['\''],
181 => ['$\mu$'],
182 => ['\P'],
183 => ['$\cdot$'],
186 => ['\textordmasculine'],
162 => ['\cent', 'wasysym'],
185 => ['$^1$'],
178 => ['$^2$'],
179 => ['$^3$'],
189 => ['$\frac{1}{2}$'],
188 => ['$\frac{1}{4}$'],
190 => ['$\frac{3}{4}'],
192 => ['\`A'],
193 => ['\\\'A'],
194 => ['\^A'],
195 => ['\~A'],
196 => ['\"A'],
197 => ['\AA'],
198 => ['\AE'],
199 => ['\cC'],
200 => ['\`E'],
201 => ['\\\'E'],
202 => ['\^E'],
203 => ['\"E'],
204 => ['\`I'],
205 => ['\\\'I'],
206 => ['\^I'],
207 => ['\"I'],
208 => ['$\eth$', 'amssymb'],
209 => ['\~N'],
210 => ['\`O'],
211 => ['\\\'O'],
212 => ['\^O'],
213 => ['\~O'],
214 => ['\"O'],
215 => ['$\times$'],
216 => ['\O'],
217 => ['\`U'],
218 => ['\\\'U'],
219 => ['\^U'],
220 => ['\"U'],
221 => ['\\\'Y'],
222 => ['\Thorn', 'wasysym'],
223 => ['\ss'],
224 => ['\`a'],
225 => ['\\\'a'],
226 => ['\^a'],
227 => ['\~a'],
228 => ['\"a'],
229 => ['\aa'],
230 => ['\ae'],
231 => ['\cc'],
232 => ['\`e'],
233 => ['\\\'e'],
234 => ['\^e'],
235 => ['\"e'],
236 => ['\`i'],
237 => ['\\\'i'],
238 => ['\^i'],
239 => ['\"i'],
240 => ['$\eth$'],
241 => ['\~n'],
242 => ['\`o'],
243 => ['\\\'o'],
244 => ['\^o'],
245 => ['\~o'],
246 => ['\"o'],
247 => ['$\divide$'],
248 => ['\o'],
249 => ['\`u'],
250 => ['\\\'u'],
251 => ['\^u'],
252 => ['\"u'],
253 => ['\\\'y'],
254 => ['\thorn', 'wasysym'],
255 => ['\"y'],
8201 => ['\thinspace'],
8194 => ['\hskip .5em\relax'],
8195 => ['\quad'],
} # :nodoc:
ENTITY_CONV_TABLE.each_value {|v| v[0] = "#{v[0]}{}" }
def entity_to_latex(entity)
text, package = ENTITY_CONV_TABLE[entity.code_point]
if text
@data[:packages] << package if package
text
else
warning("Couldn't find entity with code #{entity.code_point} in substitution table!")
''
end
end
def convert_entity(el, _opts)
entity_to_latex(el.value)
end
TYPOGRAPHIC_SYMS = {
mdash: '---', ndash: '--', hellip: '\ldots{}',
laquo_space: '\guillemotleft{}~', raquo_space: '~\guillemotright{}',
laquo: '\guillemotleft{}', raquo: '\guillemotright{}'
} # :nodoc:
def convert_typographic_sym(el, _opts)
if (result = @options[:typographic_symbols][el.value])
escape(result)
else
TYPOGRAPHIC_SYMS[el.value]
end
end
def convert_smart_quote(el, opts)
res = entity_to_latex(smart_quote_entity(el)).chomp('{}')
res << "{}" if ((nel = opts[:parent].children[opts[:index] + 1]) && nel.type == :smart_quote) || res =~ /\w$/
res
end
def convert_math(el, _opts)
@data[:packages] += %w[amssymb amsmath amsthm amsfonts]
if el.options[:category] == :block
if el.value.match?(/\A\s*\\begin\{/)
el.value
else
latex_environment('displaymath', el, el.value)
end
else
"$#{el.value}$"
end
end
def convert_abbreviation(el, _opts)
@data[:packages] += %w[acronym]
"\\ac{#{normalize_abbreviation_key(el.value)}}"
end
# Normalize the abbreviation key so that it only contains allowed ASCII character
def normalize_abbreviation_key(key)
key.gsub(/\W/) {|m| m.unpack1('H*') }
end
# Wrap the +text+ inside a LaTeX environment of type +type+. The element +el+ is passed on to
# the method #attribute_list -- the resulting string is appended to both the \\begin and the
# \\end lines of the LaTeX environment for easier post-processing of LaTeX environments.
def latex_environment(type, el, text)
attrs = attribute_list(el)
"\\begin{#{type}}#{latex_link_target(el)}#{attrs}\n#{text.rstrip}\n\\end{#{type}}#{attrs}\n"
end
# Return a string containing a valid \hypertarget command if the element has an ID defined, or
# +nil+ otherwise. If the parameter +add_label+ is +true+, a \label command will also be used
# additionally to the \hypertarget command.
def latex_link_target(el, add_label = false)
if (id = el.attr['id'])
"\\hypertarget{#{id}}{}#{add_label ? "\\label{#{id}}" : ''}"
else
nil
end
end
# Return a LaTeX comment containing all attributes as 'key="value"' pairs.
def attribute_list(el)
attrs = el.attr.map {|k, v| v.nil? ? '' : " #{k}=\"#{v}\"" }.compact.sort.join
attrs = " % #{attrs}" unless attrs.empty?
attrs
end
ESCAPE_MAP = {
"^" => "\\^{}",
"\\" => "\\textbackslash{}",
"~" => "\\ensuremath{\\sim}",
"|" => "\\textbar{}",
"<" => "\\textless{}",
">" => "\\textgreater{}",
"[" => "{[}",
"]" => "{]}",
}.merge(Hash[*"{}$%&_#".each_char.map {|c| [c, "\\#{c}"] }.flatten]) # :nodoc:
ESCAPE_RE = Regexp.union(*ESCAPE_MAP.collect {|k, _v| k }) # :nodoc:
# Escape the special LaTeX characters in the string +str+.
def escape(str)
str.gsub(ESCAPE_RE) {|m| ESCAPE_MAP[m] }
end
end
end
end
@@ -0,0 +1,300 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2026 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
require 'kramdown/converter'
module Kramdown
module Converter
# Converts a Kramdown::Document to a manpage in groff format. See man(7), groff_man(7) and
# man-pages(7) for information regarding the output.
class Man < Base
def convert(el, opts = {indent: 0, result: +''}) # :nodoc:
send("convert_#{el.type}", el, opts)
end
private
def inner(el, opts, use = :all)
arr = el.children.reject {|e| e.type == :blank }
arr.each_with_index do |inner_el, index|
next if use == :rest && index == 0
break if use == :first && index > 0
options = opts.dup
options[:parent] = el
options[:index] = index
options[:prev] = (index == 0 ? nil : arr[index - 1])
options[:next] = (index == arr.length - 1 ? nil : arr[index + 1])
convert(inner_el, options)
end
end
def convert_root(el, opts)
@title_done = false
opts[:result] = +".\\\" generated by kramdown\n"
inner(el, opts)
opts[:result]
end
def convert_blank(*)
end
alias convert_hr convert_blank
alias convert_xml_pi convert_blank
def convert_p(el, opts)
if (opts[:index] != 0 && opts[:prev].type != :header) ||
(opts[:parent].type == :blockquote && opts[:index] == 0)
opts[:result] << macro("P")
end
inner(el, opts)
newline(opts[:result])
end
def convert_header(el, opts)
return unless opts[:parent].type == :root
case el.options[:level]
when 1
unless @title_done
@title_done = true
data = el.options[:raw_text].scan(/([^(]+)\s*\((\d\w*)\)(?:\s*-+\s*(.*))?/).first ||
el.options[:raw_text].scan(/([^\s]+)\s*(?:-*\s+)?()(.*)/).first
return unless data && data[0]
name = data[0]
section = (data[1].to_s.empty? ? el.attr['data-section'] || '7' : data[1])
description = (data[2].to_s.empty? ? nil : " - #{data[2]}")
date = el.attr['data-date'] ? quote(el.attr['data-date']) : nil
extra = (el.attr['data-extra'] ? quote(escape(el.attr['data-extra'].to_s)) : nil)
opts[:result] << macro("TH", quote(escape(name.upcase)), quote(section), date, extra)
if description
opts[:result] << macro("SH", "NAME") << escape("#{name}#{description}") << "\n"
end
end
when 2
opts[:result] << macro("SH", quote(escape(el.options[:raw_text])))
when 3
opts[:result] << macro("SS", quote(escape(el.options[:raw_text])))
else
warning("Header levels greater than three are not supported")
end
end
def convert_codeblock(el, opts)
opts[:result] << macro("sp") << macro("RS", 4) << macro("EX")
opts[:result] << newline(escape(el.value, true))
opts[:result] << macro("EE") << macro("RE")
end
def convert_blockquote(el, opts)
opts[:result] << macro("RS")
inner(el, opts)
opts[:result] << macro("RE")
end
def convert_ul(el, opts)
compact = (el.attr['class'] =~ /\bcompact\b/)
opts[:result] << macro("sp") << macro("PD", 0) if compact
inner(el, opts)
opts[:result] << macro("PD") if compact
end
alias convert_dl convert_ul
alias convert_ol convert_ul
def convert_li(el, opts)
sym = (opts[:parent].type == :ul ? '\(bu' : "#{opts[:index] + 1}.")
opts[:result] << macro("IP", sym, 4)
inner(el, opts, :first)
if el.children.size > 1
opts[:result] << macro("RS")
inner(el, opts, :rest)
opts[:result] << macro("RE")
end
end
def convert_dt(el, opts)
opts[:result] << macro(opts[:prev] && opts[:prev].type == :dt ? "TQ" : "TP")
inner(el, opts)
opts[:result] << "\n"
end
def convert_dd(el, opts)
inner(el, opts, :first)
if el.children.size > 1
opts[:result] << macro("RS")
inner(el, opts, :rest)
opts[:result] << macro("RE")
end
opts[:result] << macro("sp") if opts[:next] && opts[:next].type == :dd
end
TABLE_CELL_ALIGNMENT = {left: 'l', center: 'c', right: 'r', default: 'l'}
def convert_table(el, opts)
opts[:alignment] = el.options[:alignment].map {|a| TABLE_CELL_ALIGNMENT[a] }
table_options = ["box"]
table_options << "center" if el.attr['class']&.match?(/\bcenter\b/)
opts[:result] << macro("TS") << "#{table_options.join(' ')} ;\n"
inner(el, opts)
opts[:result] << macro("TE") << macro("sp")
end
def convert_thead(el, opts)
opts[:result] << opts[:alignment].map {|a| "#{a}b" }.join(' ') << " .\n"
inner(el, opts)
opts[:result] << "=\n"
end
def convert_tbody(el, opts)
opts[:result] << ".T&\n" if opts[:index] != 0
opts[:result] << opts[:alignment].join(' ') << " .\n"
inner(el, opts)
opts[:result] << (opts[:next].type == :tfoot ? "=\n" : "_\n") if opts[:next]
end
def convert_tfoot(el, opts)
inner(el, opts)
end
def convert_tr(el, opts)
inner(el, opts)
opts[:result] << "\n"
end
def convert_td(el, opts)
result = opts[:result]
opts[:result] = +''
inner(el, opts)
if opts[:result].include?("\n")
warning("Table cells using links are not supported")
result << "\t"
else
result << opts[:result] << "\t"
end
end
def convert_html_element(*)
warning("HTML elements are not supported")
end
def convert_xml_comment(el, opts)
newline(opts[:result]) << ".\"#{escape(el.value, true).rstrip.gsub(/\n/, "\n.\"")}\n"
end
alias convert_comment convert_xml_comment
def convert_a(el, opts)
if el.children.size == 1 && el.children[0].type == :text &&
el.attr['href'] == el.children[0].value
newline(opts[:result]) << macro("UR", escape(el.attr['href'])) << macro("UE")
elsif el.attr['href'].start_with?('mailto:')
newline(opts[:result]) << macro("MT", escape(el.attr['href'].sub(/^mailto:/, ''))) <<
macro("UE")
else
newline(opts[:result]) << macro("UR", escape(el.attr['href']))
inner(el, opts)
newline(opts[:result]) << macro("UE")
end
end
def convert_img(_el, _opts)
warning("Images are not supported")
end
def convert_em(el, opts)
opts[:result] << '\fI'
inner(el, opts)
opts[:result] << '\fP'
end
def convert_strong(el, opts)
opts[:result] << '\fB'
inner(el, opts)
opts[:result] << '\fP'
end
def convert_codespan(el, opts)
opts[:result] << "\\fB#{escape(el.value)}\\fP"
end
def convert_br(_el, opts)
newline(opts[:result]) << macro("br")
end
def convert_abbreviation(el, opts)
opts[:result] << escape(el.value)
end
def convert_math(el, opts)
if el.options[:category] == :block
convert_codeblock(el, opts)
else
convert_codespan(el, opts)
end
end
def convert_footnote(*)
warning("Footnotes are not supported")
end
def convert_raw(*)
warning("Raw content is not supported")
end
def convert_text(el, opts)
text = escape(el.value)
text.lstrip! if opts[:result][-1] == "\n"
opts[:result] << text
end
def convert_entity(el, opts)
opts[:result] << unicode_char(el.value.code_point)
end
def convert_smart_quote(el, opts)
opts[:result] << unicode_char(::Kramdown::Utils::Entities.entity(el.value.to_s).code_point)
end
TYPOGRAPHIC_SYMS_MAP = {
mdash: '\(em', ndash: '\(em', hellip: '\.\.\.',
laquo_space: '\[Fo]', raquo_space: '\[Fc]', laquo: '\[Fo]', raquo: '\[Fc]'
}
def convert_typographic_sym(el, opts)
opts[:result] << TYPOGRAPHIC_SYMS_MAP[el.value]
end
def macro(name, *args)
".#{[name, *args].compact.join(' ')}\n"
end
def newline(text)
text << "\n" unless text[-1] == "\n"
text
end
def quote(text)
"\"#{text.gsub(/"/, '\\"')}\""
end
def escape(text, preserve_whitespace = false)
text = (preserve_whitespace ? text.dup : text.gsub(/\s+/, ' '))
text.gsub!('\\', "\\e")
text.gsub!(/^\./, '\\\\&.')
text.gsub!(/[.'-]/) {|m| "\\#{m}" }
text
end
def unicode_char(codepoint)
"\\[u#{codepoint.to_s(16).rjust(4, '0')}]"
end
end
end
end
@@ -0,0 +1,32 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2026 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
module Kramdown::Converter::MathEngine
# Uses the MathJax javascript library for displaying math.
#
# Note that the javascript library itself is not include or linked, this has to be done
# separately. Only the math content is marked up correctly.
module Mathjax
def self.call(converter, el, opts)
value = converter.escape_html(el.value)
result = el.options[:category] == :block ? "\\[#{value}\\]\n" : "\\(#{value}\\)"
if el.attr.empty?
result
elsif el.options[:category] == :block
converter.format_as_block_html('div', el.attr, result, opts[:indent])
else
converter.format_as_span_html('span', el.attr, "$#{el.value}$")
end
end
end
end
@@ -0,0 +1,58 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2026 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
require 'kramdown/converter'
module Kramdown
module Converter
# Removes all block (and optionally span) level HTML tags from the element tree.
#
# This converter can be used on parsed HTML documents to get an element tree that will only
# contain native kramdown elements.
#
# *Note* that the returned element tree may not be fully conformant (i.e. the content models of
# *some elements may be violated)!
#
# This converter modifies the given tree in-place and returns it.
class RemoveHtmlTags < Base
def initialize(root, options)
super
@options[:template] = ''
end
def convert(el)
real_el, el = el, el.value if el.type == :footnote
children = el.children.dup
index = 0
while index < children.length
if children[index].type == :xml_pi ||
(children[index].type == :html_element && (children[index].value == 'style' ||
children[index].value == 'script'))
children[index..index] = []
elsif children[index].type == :html_element &&
((@options[:remove_block_html_tags] && children[index].options[:category] == :block) ||
(@options[:remove_span_html_tags] && children[index].options[:category] == :span))
children[index..index] = children[index].children
else
convert(children[index])
index += 1
end
end
el.children = children
real_el || el
end
end
end
end
@@ -0,0 +1,56 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2026 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
module Kramdown
module Converter
# == Container for Syntax Highlighters
#
# This module serves as container for the syntax highlighters that can be used together with
# kramdown.
#
# A syntax highlighter should not store any data itself but should use the provided converter
# object to do so (See Kramdown::Converter::Base#data).
#
# == Implementing a Syntax Highlighter
#
# Implementing a new syntax highlighter is easy because it is just an object that needs to
# respond to #call.
#
# The method #call needs to take the following arguments:
#
# converter:: This argument contains the converter object that calls the syntax highlighter. It
# can be used, for example, to store data in Kramdown::Converter::Base#data for one
# conversion run.
#
# text:: The raw text that should be highlighted.
#
# lang:: The language that the text should be highlighted for (e.g. ruby, python, ...).
#
# type:: The type of text, either :span for span-level code or :block for a codeblock.
#
# opts:: A Hash with options that may be passed from the converter.
#
# The return value of the method should be the highlighted text, suitable for the given
# converter (e.g. HTML for the HTML converter).
#
# == Special Implementation Details
#
# HTML converter:: If the syntax highlighter is used with an HTML converter, it should return
# :block type text correctly wrapped (i.e. normally inside a pre-tag, but may
# also be a table-tag or just a div-tag) but :span type text *without* a
# code-tag!
#
# Also, a syntax highlighter should store the default highlighting language for
# the invocation in the +opts+ hash under the key :default_lang.
module SyntaxHighlighter
end
end
end
@@ -0,0 +1,35 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2026 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
module Kramdown::Converter::SyntaxHighlighter
# Uses Minted to highlight code blocks and code spans.
module Minted
def self.call(converter, text, lang, type, _opts)
opts = converter.options[:syntax_highlighter_opts]
# Fallback to default language
lang ||= opts[:default_lang]
options = []
options << "breaklines" if opts[:wrap]
options << "linenos" if opts[:line_numbers]
options << "frame=#{opts[:frame]}" if opts[:frame]
if lang && type == :block
"\\begin{minted}[#{options.join(',')}]{#{lang}}\n#{text}\n\\end{minted}"
elsif lang && type == :span
"\\mintinline{#{lang}}{#{text}}"
else
nil
end
end
end
end
@@ -0,0 +1,85 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2026 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
module Kramdown::Converter::SyntaxHighlighter
# Uses Rouge which is CSS-compatible to Pygments to highlight code blocks and code spans.
module Rouge
begin
require 'rouge'
# Highlighting via Rouge is available if this constant is +true+.
AVAILABLE = true
rescue LoadError, SyntaxError
AVAILABLE = false # :nodoc:
end
def self.call(converter, text, lang, type, call_opts)
opts = options(converter, type)
call_opts[:default_lang] = opts[:default_lang]
return nil unless lang || opts[:default_lang] || opts[:guess_lang]
lexer = ::Rouge::Lexer.find_fancy(lang || opts[:default_lang], text)
return nil if opts[:disable] || !lexer || (lexer.tag == "plaintext" && !opts[:guess_lang])
opts[:css_class] ||= 'highlight' # For backward compatibility when using Rouge 2.0
formatter = formatter_class(opts).new(opts)
formatter.format(lexer.lex(text))
end
def self.options(converter, type)
prepare_options(converter)
converter.data[:syntax_highlighter_rouge][type]
end
def self.prepare_options(converter)
return if converter.data.key?(:syntax_highlighter_rouge)
cache = converter.data[:syntax_highlighter_rouge] = {}
opts = converter.options[:syntax_highlighter_opts].dup
span_opts = opts.delete(:span)&.dup || {}
block_opts = opts.delete(:block)&.dup || {}
normalize_keys(span_opts)
normalize_keys(block_opts)
cache[:span] = opts.merge(span_opts)
cache[:span][:wrap] = false
cache[:block] = opts.merge(block_opts)
end
def self.normalize_keys(hash)
return if hash.empty?
hash.keys.each do |k|
hash[k.kind_of?(String) ? Kramdown::Options.str_to_sym(k) : k] = hash.delete(k)
end
end
def self.formatter_class(opts = {})
case formatter = opts[:formatter]
when Class
formatter
when /\A[[:upper:]][[:alnum:]_]*\z/
::Rouge::Formatters.const_get(formatter, false)
else
# Available in Rouge 2.0 or later
::Rouge::Formatters::HTMLLegacy
end
rescue NameError
# Fallback to Rouge 1.x
::Rouge::Formatters::HTML
end
end
end
@@ -0,0 +1,69 @@
# -*- coding: utf-8; frozen_string_literal: true -*-
#
#--
# Copyright (C) 2009-2026 Thomas Leitner <t_leitner@gmx.at>
#
# This file is part of kramdown which is licensed under the MIT.
#++
#
require 'kramdown/converter'
module Kramdown
module Converter
# Converts a Kramdown::Document to an element tree that represents the table of contents.
#
# The returned tree consists of Element objects of type :toc where the root element is just used
# as container object. Each :toc element contains as value the wrapped :header element and under
# the attribute key :id the header ID that should be used (note that this ID may not exist in
# the wrapped element).
#
# Since the TOC tree consists of special :toc elements, one cannot directly feed this tree to
# other converters!
class Toc < Base
def initialize(root, options)
super
@toc = Element.new(:toc)
@stack = []
@options[:template] = ''
end
def convert(el)
if el.type == :header && in_toc?(el)
attr = el.attr.dup
attr['id'] = generate_id(el.options[:raw_text]) if @options[:auto_ids] && !attr['id']
add_to_toc(el, attr['id']) if attr['id']
else
el.children.each {|child| convert(child) }
end
@toc
end
private
def add_to_toc(el, id)
toc_element = Element.new(:toc, el, id: id)
success = false
until success
if @stack.empty?
@toc.children << toc_element
@stack << toc_element
success = true
elsif @stack.last.value.options[:level] < el.options[:level]
@stack.last.children << toc_element
@stack << toc_element
success = true
else
@stack.pop
end
end
end
end
end
end