This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
# -*- coding: utf-8; frozen_string_literal: true -*-
|
||||
#
|
||||
#--
|
||||
# Copyright (C) 2009-2019 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-2019 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-2019 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}\">⋅</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-2019 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
|
||||
""
|
||||
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(/"/, '"') + '"'
|
||||
end
|
||||
|
||||
# :startdoc:
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,626 @@
|
||||
# -*- coding: utf-8; frozen_string_literal: true -*-
|
||||
#
|
||||
#--
|
||||
# Copyright (C) 2009-2019 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-2019 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-2019 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-2019 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-2019 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-2019 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-2019 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-2019 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
|
||||
Reference in New Issue
Block a user