# -*- coding: utf-8 -*-##--# Copyright (C) 2009-2014 Thomas Leitner <t_leitner@gmx.at>## This file is part of kramdown which is licensed under the MIT.#++#require'erb'require'kramdown/utils'moduleKramdownmoduleConverter# == \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!classBase# 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.definitialize(root,options)@options=options@root=root@data={}@warnings=[]endprivate_class_method(:new,:allocate)# Returns whether the template should be applied before the conversion of the tree.## Defaults to false.defapply_template_before?falseend# Returns whether the template should be applied ater the conversion of the tree.## Defaults to true.defapply_template_after?trueend# 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.defself.convert(tree,options={})converter=new(tree,::Kramdown::Options.merge(options.merge(tree.options[:options]||{})))apply_template(converter,'')if!converter.options[:template].empty?&&converter.apply_template_before?result=converter.convert(tree)result.encode!(tree.options[:encoding])ifresult.respond_to?(:encode!)&&result.encoding!=Encoding::BINARYresult=apply_template(converter,result)if!converter.options[:template].empty?&&converter.apply_template_after?[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!defconvert(el)raiseNotImplementedErrorend# 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.defself.apply_template(converter,body)# :nodoc:erb=ERB.new(get_template(converter.options[:template]))obj=Object.newobj.instance_variable_set(:@converter,converter)obj.instance_variable_set(:@body,body)erb.result(obj.instance_eval{binding})end# Return the template specified by +template+.defself.get_template(template)#DEPRECATED: use content of #get_template_new in 2.0format_ext='.'+self.name.split(/::/).last.downcaseshipped=File.join(::Kramdown.data_dir,template+format_ext)ifFile.exist?(template)File.read(template)elsifFile.exist?(template+format_ext)File.read(template+format_ext)elsifFile.exist?(shipped)File.read(shipped)elsiftemplate.start_with?('string://')template.sub(/\Astring:\/\//,'')elseget_template_new(template)endenddefself.get_template_new(template)# :nodoc:format_ext='.'+::Kramdown::Utils.snake_case(self.name.split(/::/).last)shipped=File.join(::Kramdown.data_dir,template+format_ext)ifFile.exist?(template)File.read(template)elsifFile.exist?(template+format_ext)File.read(template+format_ext)elsifFile.exist?(shipped)File.read(shipped)elsiftemplate.start_with?('string://')template.sub(/\Astring:\/\//,'')elseraise"The specified template file #{template} does not exist"endend# Add the given warning +text+ to the warning array.defwarning(text)@warnings<<textend# Return +true+ if the header element +el+ should be used for the table of contents (as# specified by the +toc_levels+ option).defin_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.defoutput_header_level(level)[[level+@options[:header_offset],6].min,1].maxend# Extract the code block/span language from the attributes.defextract_code_language(attr)ifattr['class']&&attr['class']=~/\blanguage-\w+\b/attr['class'].scan(/\blanguage-(\w+)\b/).first.firstendend# See #extract_code_language## *Warning*: This version will modify the given attributes if a language is present.defextract_code_language!(attr)lang=extract_code_language(attr)attr['class']=attr['class'].sub(/\blanguage-\w+\b/,'').stripiflangattr.delete('class')iflang&&attr['class'].empty?langend# Highlight the given +text+ in the language +lang+ with the syntax highlighter configured# through the option 'syntax_highlighter'.defhighlight_code(text,lang,type,opts={})returnnilunless@options[:syntax_highlighter]highlighter=::Kramdown::Converter.syntax_highlighter(@options[:syntax_highlighter])ifhighlighterhighlighter.call(self,text,lang,type,opts)elsewarning("The configured syntax highlighter #{@options[:syntax_highlighter]} is not available.")nilendend# Format the given math element with the math engine configured through the option# 'math_engine'.defformat_math(el,opts={})returnnilunless@options[:math_engine]engine=::Kramdown::Converter.math_engine(@options[:math_engine])ifengineengine.call(self,el,opts)elsewarning("The configured math engine #{@options[:math_engine]} is not available.")nilendend# 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.defgenerate_id(str)str=::Kramdown::Utils::Unidecoder.decode(str)if@options[:transliterated_header_ids]gen_id=str.gsub(/^[^a-zA-Z]+/,'')gen_id.tr!('^a-zA-Z0-9 -','')gen_id.tr!(' ','-')gen_id.downcase!gen_id='section'ifgen_id.length==0@used_ids||={}if@used_ids.has_key?(gen_id)gen_id+='-'<<(@used_ids[gen_id]+=1).to_selse@used_ids[gen_id]=0end@options[:auto_id_prefix]+gen_idendSMART_QUOTE_INDICES={:lsquo=>0,:rsquo=>1,:ldquo=>2,:rdquo=>3}# :nodoc:# Return the entity that represents the given smart_quote element.defsmart_quote_entity(el)res=@options[:smart_quotes][SMART_QUOTE_INDICES[el.value]]::Kramdown::Utils::Entities.entity(res)endendendend