257 lines
9.5 KiB
Ruby
257 lines
9.5 KiB
Ruby
|
# -*- 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])
|
||
|
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(self.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.sub(/\Astring:\/\//, '')
|
||
|
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
|