File: base.rb

package info (click to toggle)
ruby-kramdown 2.5.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, trixie
  • size: 2,896 kB
  • sloc: ruby: 6,462; makefile: 10
file content (257 lines) | stat: -rw-r--r-- 9,846 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
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