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
|
||||
Reference in New Issue
Block a user