Class: Pandocomatic::ConvertFileCommand

Inherits:
Command
  • Object
show all
Defined in:
lib/pandocomatic/command/convert_file_command.rb

Overview

Command to convert a file

Constant Summary collapse

INTERNAL_TEMPLATE =

Name of the anonymous inner template.

'internal template'

Instance Attribute Summary collapse

Attributes inherited from Command

#errors, #index

Instance Method Summary collapse

Methods inherited from Command

#all_errors, #count, #directory?, #dry_run?, #errors?, #execute, #file_modified?, #index_to_s, #make_quiet, #modified_only?, #multiple?, #quiet?, reset, #runnable?, #skip?, #src_root, #uncount

Constructor Details

#initialize(config, src, dst, template_name = nil) ⇒ ConvertFileCommand

Create a new ConvertFileCommand

Parameters:

  • config (Configuration)

    pandocomatic’s configuration

  • src (String)

    the path to the file to convert

  • dst (String)

    the path to save the output of the conversion

  • template_name (String = nil) (defaults to: nil)

    the template to use while converting this file



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/pandocomatic/command/convert_file_command.rb', line 92

def initialize(config, src, dst, template_name = nil)
  super()

  @config = config
  @src = src
  @dst = dst

  @template_name = if template_name.nil? || template_name.empty?
                     @config.determine_template @src
                   else
                     template_name
                   end
  @metadata = @config. @src, @template_name
  @dst = @config.set_destination @dst, @template_name, @metadata

  @errors.push IOError.new(:file_does_not_exist, nil, @src) unless File.exist? @src
  @errors.push IOError.new(:file_is_not_a_file, nil, @src) unless File.file? @src
  @errors.push IOError.new(:file_is_not_readable, nil, @src) unless File.readable? @src
end

Instance Attribute Details

#configConfiguration

Returns the configuration of pandocomatic used to convert the file.

Returns:

  • (Configuration)

    the configuration of pandocomatic used to convert the file



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
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/pandocomatic/command/convert_file_command.rb', line 82

class ConvertFileCommand < Command
  attr_reader :config, :src, :dst

  # Create a new ConvertFileCommand
  #
  # @param config [Configuration] pandocomatic's configuration
  # @param src [String] the path to the file to convert
  # @param dst [String] the path to save the output of the conversion
  # @param template_name [String = nil] the template to use while converting
  #   this file
  def initialize(config, src, dst, template_name = nil)
    super()

    @config = config
    @src = src
    @dst = dst

    @template_name = if template_name.nil? || template_name.empty?
                       @config.determine_template @src
                     else
                       template_name
                     end
    @metadata = @config. @src, @template_name
    @dst = @config.set_destination @dst, @template_name, @metadata

    @errors.push IOError.new(:file_does_not_exist, nil, @src) unless File.exist? @src
    @errors.push IOError.new(:file_is_not_a_file, nil, @src) unless File.file? @src
    @errors.push IOError.new(:file_is_not_readable, nil, @src) unless File.readable? @src
  end

  # Execute this ConvertFileCommand
  def run
    convert_file
  end

  # Create a string representation of this ConvertFileCommand
  #
  # @return [String]
  def to_s
    str = "convert #{File.basename @src} #{"-> #{File.basename @dst}" unless @dst.nil?}"
    unless @metadata.unique?
      str += "\n\t encountered multiple YAML metadata blocks with a pandocomatic property. " \
             'Only the pandocomatic property in the first YAML metadata block is being used; ' \
             'the others are discarded.'
    end
    str
  end

  # Name of the anonymous inner template.
  INTERNAL_TEMPLATE = 'internal template'

  private

  def convert_file
    pandoc_options = @metadata.pandoc_options || {}
    template = nil

    # Determine the actual options and settings to use when converting this
    # file.
    if !@template_name.nil? && !@template_name.empty?
      unless @config.template? @template_name
        raise ConfigurationError.new(:no_such_template, nil,
                                     @template_name)
      end

      template = @config.get_template @template_name
      pandoc_options = Template.extend_value(pandoc_options, template.pandoc)
    else
      template = Template.new INTERNAL_TEMPLATE
    end

    if template.name == INTERNAL_TEMPLATE
      Pandocomatic::LOG.debug '  #  Using internal template.'
    else
      Pandocomatic::LOG.debug "  #  Using template '#{template.name}'."
    end

    # Ignore the `--verbose` option, and warn about ignoring it
    if pandoc_options.key?('verbose') && !@config.feature_enabled?(:pandoc_verbose)
      pandoc_options.delete 'verbose'
      warn 'WARNING: Ignoring the pandoc option "--verbose" because it ' \
           'might interfere with the working of pandocomatic. If you want to use ' \
           '"--verbose" anyway, use pandocomatic\'s feature toggle ' \
           '"--enable pandoc-verbose".'
    end

    template_log = Pandocomatic::LOG.indent(YAML.dump(template.to_h).sub('---', ''), 34)

    if @metadata.pandocomatic?
      template.merge! Template.new(INTERNAL_TEMPLATE, @metadata.pandocomatic)

      Pandocomatic::LOG.debug '  #  Selected template mixed with internal template and pandocomatic metadata ' \
                              "gives final template:#{template_log}"
    else
      Pandocomatic::LOG.debug "  #  Selected template:#{template_log}"
    end

    # Write out the results of the conversion process to file.
    @dst = @metadata.pandoc_options['output'] if @dst.to_s.empty? && @metadata.pandoc_options.key?('output')

    # Run setup scripts
    setup template

    # Read in the file to convert
    Pandocomatic::LOG.debug "  →  Reading source file: '#{@src}'"
    input = File.read @src

    # Run the default preprocessors to mix-in information about the file
    # that is being converted and mix-in the template's metadata section as
    # well
    input = FileInfoPreprocessor.run input, @src, src_root, pandoc_options
    input = MetadataPreprocessor.run input, template. if template.metadata?

    # Convert the file by preprocessing it, run pandoc on it, and
    # postprocessing the output
    input = preprocess input, template
    input = pandoc input, pandoc_options, File.dirname(@src)
    output = postprocess input, template

    begin
      # Either output to file or to STDOUT.
      if @config.stdout?
        Pandocomatic::LOG.debug '  ←  Writing output to STDOUT.'
        puts output
        @dst.close!
      else
        unless use_output_option @dst
          Pandocomatic::LOG.debug "  ←  Writing output to '#{@dst}'."
          File.open(@dst, 'w') do |file|
            raise IOError.new(:file_is_not_a_file, nil, @dst) unless File.file? @dst
            raise IOError.new(:file_is_not_writable, nil, @dst) unless File.writable? @dst

            file << output
          end
        end
      end
    rescue StandardError => e
      raise IOError.new(:error_writing_file, e, @dst)
    end

    # run cleanup scripts
    cleanup template
  end

  def pandoc(input, options, src_dir)
    absolute_dst = File.expand_path @dst
    Dir.chdir(src_dir) do
      Pandocomatic::LOG.debug "     #  Changing directory to '#{src_dir}'"
      converter = Paru::Pandoc.new
      options.each do |option, value|
        # Options come from a YAML string. In YAML, properties without a value get value nil.
        # Interpret these empty properties as "skip this property"
        next if value.nil?

        if PANDOC_OPTIONS_WITH_PATH.include? option
          executable = option == 'filter'
          value = if value.is_a? Array
                    value.map { |v| Path.update_path(@config, v, absolute_dst, check_executable: executable) }
                  else
                    Path.update_path(@config, value, @dst, check_executable: executable)
                  end
        end

        # There is no "pdf" output format; change it to latex but keep the
        # extension.
        value = determine_output_for_pdf(options) if (option == 'to') && (value == 'pdf')

        begin
          # Pandoc multi-word options can have the multiple words separated by
          # both underscore (_) and dash (-).
          option = option.gsub '-', '_'
          converter.send option, value unless
                  (option == 'output') ||
                  (option == 'use_extension') ||
                  (option == 'rename')
          # don't let pandoc write the output to enable postprocessing
        rescue StandardError
          Pandocomatic::LOG.warn "WARNING: The pandoc option '#{option}'"
          " (with value '#{value}') is not recognized by paru. This option is skipped."
        end
      end

      converter.send 'output', absolute_dst if use_output_option absolute_dst

      begin
        Pandocomatic::LOG.debug '     #  Running pandoc'
        Pandocomatic::LOG.debug "     |  #{Pandocomatic::LOG.indent(converter.to_command, 43)}"
        converter << input
      rescue Paru::Error => e
        raise PandocError.new(:error_running_pandoc, e, input)
      end
    end
  end

  # Preprocess the input
  #
  # @param input [String] the input to preprocess
  # @param template [Template] template
  #
  # @return [String] the generated output
  def preprocess(input, template)
    process input, Template::PREPROCESSORS, template
  end

  # Postprocess the input
  #
  # @param input [String] the input to postprocess
  # @param template [Template] template
  #
  # @return [String] the generated output
  def postprocess(input, template)
    process input, Template::POSTPROCESSORS, template
  end

  # Run setup scripts
  #
  # @param template [Template] template
  def setup(template)
    process '', Template::SETUP, template
  end

  # Run cleanup scripts
  #
  # @param template [Template] template
  def cleanup(template)
    process '', Template::CLEANUP, template
  end

  # Run the input string through a list of filters called processors. There
  # are various types: preprocessors and postprocessors, setup and
  # cleanup, and rename
  def process(input, type, template)
    if template.send "#{type}?"
      processors = template.send type
      Pandocomatic::LOG.debug "     #  Running #{type}:" unless processors.empty?
      output = input
      processors.each do |processor|
        script = if Path.local_path? processor
                   processor
                 else
                   Path.update_path(@config, processor, @dst, check_executable: true)
                 end

        command, *parameters = script.shellsplit # split on spaces unless it is preceded by a backslash

        unless File.exist? command
          command = Path.which(command)
          script = "#{command} #{parameters.join(' ')}"

          raise ProcessorError.new(:script_does_not_exist, nil, command) if command.nil?
        end

        raise ProcessorError.new(:script_is_not_executable, nil, command) unless File.executable? command

        begin
          Pandocomatic::LOG.debug "     |  #{script}"
          output = Processor.run(script, output)
        rescue StandardError => e
          ProcessorError.new(:error_processing_script, e, [script, @src])
        end
      end
      output
    else
      input
    end
  end

  def use_output_option(dst)
    OUTPUT_FORMATS.include?(File.extname(dst).slice(1..-1))
  end

  # Pandoc version 2 supports multiple pdf engines. Determine which
  # to use given the options.
  #
  # @param options [Hash] the options to a paru pandoc converter
  # @return [String] the output format for the pdf engine to use.
  def determine_output_for_pdf(options)
    if options.key? 'pdf-engine'
      engine = options['pdf-engine']
      case engine
      when 'context'
        'context'
      when 'pdfroff'
        'ms'
      when 'wkhtmltopdf', 'weasyprint', 'prince'
        'html'
      else
        'latex'
      end
    else
      # According to pandoc's manual, the default is LaTeX
      'latex'
    end
  end
end

#dstString

Returns the path to the output file.

Returns:

  • (String)

    the path to the output file



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
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/pandocomatic/command/convert_file_command.rb', line 82

class ConvertFileCommand < Command
  attr_reader :config, :src, :dst

  # Create a new ConvertFileCommand
  #
  # @param config [Configuration] pandocomatic's configuration
  # @param src [String] the path to the file to convert
  # @param dst [String] the path to save the output of the conversion
  # @param template_name [String = nil] the template to use while converting
  #   this file
  def initialize(config, src, dst, template_name = nil)
    super()

    @config = config
    @src = src
    @dst = dst

    @template_name = if template_name.nil? || template_name.empty?
                       @config.determine_template @src
                     else
                       template_name
                     end
    @metadata = @config. @src, @template_name
    @dst = @config.set_destination @dst, @template_name, @metadata

    @errors.push IOError.new(:file_does_not_exist, nil, @src) unless File.exist? @src
    @errors.push IOError.new(:file_is_not_a_file, nil, @src) unless File.file? @src
    @errors.push IOError.new(:file_is_not_readable, nil, @src) unless File.readable? @src
  end

  # Execute this ConvertFileCommand
  def run
    convert_file
  end

  # Create a string representation of this ConvertFileCommand
  #
  # @return [String]
  def to_s
    str = "convert #{File.basename @src} #{"-> #{File.basename @dst}" unless @dst.nil?}"
    unless @metadata.unique?
      str += "\n\t encountered multiple YAML metadata blocks with a pandocomatic property. " \
             'Only the pandocomatic property in the first YAML metadata block is being used; ' \
             'the others are discarded.'
    end
    str
  end

  # Name of the anonymous inner template.
  INTERNAL_TEMPLATE = 'internal template'

  private

  def convert_file
    pandoc_options = @metadata.pandoc_options || {}
    template = nil

    # Determine the actual options and settings to use when converting this
    # file.
    if !@template_name.nil? && !@template_name.empty?
      unless @config.template? @template_name
        raise ConfigurationError.new(:no_such_template, nil,
                                     @template_name)
      end

      template = @config.get_template @template_name
      pandoc_options = Template.extend_value(pandoc_options, template.pandoc)
    else
      template = Template.new INTERNAL_TEMPLATE
    end

    if template.name == INTERNAL_TEMPLATE
      Pandocomatic::LOG.debug '  #  Using internal template.'
    else
      Pandocomatic::LOG.debug "  #  Using template '#{template.name}'."
    end

    # Ignore the `--verbose` option, and warn about ignoring it
    if pandoc_options.key?('verbose') && !@config.feature_enabled?(:pandoc_verbose)
      pandoc_options.delete 'verbose'
      warn 'WARNING: Ignoring the pandoc option "--verbose" because it ' \
           'might interfere with the working of pandocomatic. If you want to use ' \
           '"--verbose" anyway, use pandocomatic\'s feature toggle ' \
           '"--enable pandoc-verbose".'
    end

    template_log = Pandocomatic::LOG.indent(YAML.dump(template.to_h).sub('---', ''), 34)

    if @metadata.pandocomatic?
      template.merge! Template.new(INTERNAL_TEMPLATE, @metadata.pandocomatic)

      Pandocomatic::LOG.debug '  #  Selected template mixed with internal template and pandocomatic metadata ' \
                              "gives final template:#{template_log}"
    else
      Pandocomatic::LOG.debug "  #  Selected template:#{template_log}"
    end

    # Write out the results of the conversion process to file.
    @dst = @metadata.pandoc_options['output'] if @dst.to_s.empty? && @metadata.pandoc_options.key?('output')

    # Run setup scripts
    setup template

    # Read in the file to convert
    Pandocomatic::LOG.debug "  →  Reading source file: '#{@src}'"
    input = File.read @src

    # Run the default preprocessors to mix-in information about the file
    # that is being converted and mix-in the template's metadata section as
    # well
    input = FileInfoPreprocessor.run input, @src, src_root, pandoc_options
    input = MetadataPreprocessor.run input, template. if template.metadata?

    # Convert the file by preprocessing it, run pandoc on it, and
    # postprocessing the output
    input = preprocess input, template
    input = pandoc input, pandoc_options, File.dirname(@src)
    output = postprocess input, template

    begin
      # Either output to file or to STDOUT.
      if @config.stdout?
        Pandocomatic::LOG.debug '  ←  Writing output to STDOUT.'
        puts output
        @dst.close!
      else
        unless use_output_option @dst
          Pandocomatic::LOG.debug "  ←  Writing output to '#{@dst}'."
          File.open(@dst, 'w') do |file|
            raise IOError.new(:file_is_not_a_file, nil, @dst) unless File.file? @dst
            raise IOError.new(:file_is_not_writable, nil, @dst) unless File.writable? @dst

            file << output
          end
        end
      end
    rescue StandardError => e
      raise IOError.new(:error_writing_file, e, @dst)
    end

    # run cleanup scripts
    cleanup template
  end

  def pandoc(input, options, src_dir)
    absolute_dst = File.expand_path @dst
    Dir.chdir(src_dir) do
      Pandocomatic::LOG.debug "     #  Changing directory to '#{src_dir}'"
      converter = Paru::Pandoc.new
      options.each do |option, value|
        # Options come from a YAML string. In YAML, properties without a value get value nil.
        # Interpret these empty properties as "skip this property"
        next if value.nil?

        if PANDOC_OPTIONS_WITH_PATH.include? option
          executable = option == 'filter'
          value = if value.is_a? Array
                    value.map { |v| Path.update_path(@config, v, absolute_dst, check_executable: executable) }
                  else
                    Path.update_path(@config, value, @dst, check_executable: executable)
                  end
        end

        # There is no "pdf" output format; change it to latex but keep the
        # extension.
        value = determine_output_for_pdf(options) if (option == 'to') && (value == 'pdf')

        begin
          # Pandoc multi-word options can have the multiple words separated by
          # both underscore (_) and dash (-).
          option = option.gsub '-', '_'
          converter.send option, value unless
                  (option == 'output') ||
                  (option == 'use_extension') ||
                  (option == 'rename')
          # don't let pandoc write the output to enable postprocessing
        rescue StandardError
          Pandocomatic::LOG.warn "WARNING: The pandoc option '#{option}'"
          " (with value '#{value}') is not recognized by paru. This option is skipped."
        end
      end

      converter.send 'output', absolute_dst if use_output_option absolute_dst

      begin
        Pandocomatic::LOG.debug '     #  Running pandoc'
        Pandocomatic::LOG.debug "     |  #{Pandocomatic::LOG.indent(converter.to_command, 43)}"
        converter << input
      rescue Paru::Error => e
        raise PandocError.new(:error_running_pandoc, e, input)
      end
    end
  end

  # Preprocess the input
  #
  # @param input [String] the input to preprocess
  # @param template [Template] template
  #
  # @return [String] the generated output
  def preprocess(input, template)
    process input, Template::PREPROCESSORS, template
  end

  # Postprocess the input
  #
  # @param input [String] the input to postprocess
  # @param template [Template] template
  #
  # @return [String] the generated output
  def postprocess(input, template)
    process input, Template::POSTPROCESSORS, template
  end

  # Run setup scripts
  #
  # @param template [Template] template
  def setup(template)
    process '', Template::SETUP, template
  end

  # Run cleanup scripts
  #
  # @param template [Template] template
  def cleanup(template)
    process '', Template::CLEANUP, template
  end

  # Run the input string through a list of filters called processors. There
  # are various types: preprocessors and postprocessors, setup and
  # cleanup, and rename
  def process(input, type, template)
    if template.send "#{type}?"
      processors = template.send type
      Pandocomatic::LOG.debug "     #  Running #{type}:" unless processors.empty?
      output = input
      processors.each do |processor|
        script = if Path.local_path? processor
                   processor
                 else
                   Path.update_path(@config, processor, @dst, check_executable: true)
                 end

        command, *parameters = script.shellsplit # split on spaces unless it is preceded by a backslash

        unless File.exist? command
          command = Path.which(command)
          script = "#{command} #{parameters.join(' ')}"

          raise ProcessorError.new(:script_does_not_exist, nil, command) if command.nil?
        end

        raise ProcessorError.new(:script_is_not_executable, nil, command) unless File.executable? command

        begin
          Pandocomatic::LOG.debug "     |  #{script}"
          output = Processor.run(script, output)
        rescue StandardError => e
          ProcessorError.new(:error_processing_script, e, [script, @src])
        end
      end
      output
    else
      input
    end
  end

  def use_output_option(dst)
    OUTPUT_FORMATS.include?(File.extname(dst).slice(1..-1))
  end

  # Pandoc version 2 supports multiple pdf engines. Determine which
  # to use given the options.
  #
  # @param options [Hash] the options to a paru pandoc converter
  # @return [String] the output format for the pdf engine to use.
  def determine_output_for_pdf(options)
    if options.key? 'pdf-engine'
      engine = options['pdf-engine']
      case engine
      when 'context'
        'context'
      when 'pdfroff'
        'ms'
      when 'wkhtmltopdf', 'weasyprint', 'prince'
        'html'
      else
        'latex'
      end
    else
      # According to pandoc's manual, the default is LaTeX
      'latex'
    end
  end
end

#srcString

Returns the path to the file to convert.

Returns:

  • (String)

    the path to the file to convert



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
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/pandocomatic/command/convert_file_command.rb', line 82

class ConvertFileCommand < Command
  attr_reader :config, :src, :dst

  # Create a new ConvertFileCommand
  #
  # @param config [Configuration] pandocomatic's configuration
  # @param src [String] the path to the file to convert
  # @param dst [String] the path to save the output of the conversion
  # @param template_name [String = nil] the template to use while converting
  #   this file
  def initialize(config, src, dst, template_name = nil)
    super()

    @config = config
    @src = src
    @dst = dst

    @template_name = if template_name.nil? || template_name.empty?
                       @config.determine_template @src
                     else
                       template_name
                     end
    @metadata = @config. @src, @template_name
    @dst = @config.set_destination @dst, @template_name, @metadata

    @errors.push IOError.new(:file_does_not_exist, nil, @src) unless File.exist? @src
    @errors.push IOError.new(:file_is_not_a_file, nil, @src) unless File.file? @src
    @errors.push IOError.new(:file_is_not_readable, nil, @src) unless File.readable? @src
  end

  # Execute this ConvertFileCommand
  def run
    convert_file
  end

  # Create a string representation of this ConvertFileCommand
  #
  # @return [String]
  def to_s
    str = "convert #{File.basename @src} #{"-> #{File.basename @dst}" unless @dst.nil?}"
    unless @metadata.unique?
      str += "\n\t encountered multiple YAML metadata blocks with a pandocomatic property. " \
             'Only the pandocomatic property in the first YAML metadata block is being used; ' \
             'the others are discarded.'
    end
    str
  end

  # Name of the anonymous inner template.
  INTERNAL_TEMPLATE = 'internal template'

  private

  def convert_file
    pandoc_options = @metadata.pandoc_options || {}
    template = nil

    # Determine the actual options and settings to use when converting this
    # file.
    if !@template_name.nil? && !@template_name.empty?
      unless @config.template? @template_name
        raise ConfigurationError.new(:no_such_template, nil,
                                     @template_name)
      end

      template = @config.get_template @template_name
      pandoc_options = Template.extend_value(pandoc_options, template.pandoc)
    else
      template = Template.new INTERNAL_TEMPLATE
    end

    if template.name == INTERNAL_TEMPLATE
      Pandocomatic::LOG.debug '  #  Using internal template.'
    else
      Pandocomatic::LOG.debug "  #  Using template '#{template.name}'."
    end

    # Ignore the `--verbose` option, and warn about ignoring it
    if pandoc_options.key?('verbose') && !@config.feature_enabled?(:pandoc_verbose)
      pandoc_options.delete 'verbose'
      warn 'WARNING: Ignoring the pandoc option "--verbose" because it ' \
           'might interfere with the working of pandocomatic. If you want to use ' \
           '"--verbose" anyway, use pandocomatic\'s feature toggle ' \
           '"--enable pandoc-verbose".'
    end

    template_log = Pandocomatic::LOG.indent(YAML.dump(template.to_h).sub('---', ''), 34)

    if @metadata.pandocomatic?
      template.merge! Template.new(INTERNAL_TEMPLATE, @metadata.pandocomatic)

      Pandocomatic::LOG.debug '  #  Selected template mixed with internal template and pandocomatic metadata ' \
                              "gives final template:#{template_log}"
    else
      Pandocomatic::LOG.debug "  #  Selected template:#{template_log}"
    end

    # Write out the results of the conversion process to file.
    @dst = @metadata.pandoc_options['output'] if @dst.to_s.empty? && @metadata.pandoc_options.key?('output')

    # Run setup scripts
    setup template

    # Read in the file to convert
    Pandocomatic::LOG.debug "  →  Reading source file: '#{@src}'"
    input = File.read @src

    # Run the default preprocessors to mix-in information about the file
    # that is being converted and mix-in the template's metadata section as
    # well
    input = FileInfoPreprocessor.run input, @src, src_root, pandoc_options
    input = MetadataPreprocessor.run input, template. if template.metadata?

    # Convert the file by preprocessing it, run pandoc on it, and
    # postprocessing the output
    input = preprocess input, template
    input = pandoc input, pandoc_options, File.dirname(@src)
    output = postprocess input, template

    begin
      # Either output to file or to STDOUT.
      if @config.stdout?
        Pandocomatic::LOG.debug '  ←  Writing output to STDOUT.'
        puts output
        @dst.close!
      else
        unless use_output_option @dst
          Pandocomatic::LOG.debug "  ←  Writing output to '#{@dst}'."
          File.open(@dst, 'w') do |file|
            raise IOError.new(:file_is_not_a_file, nil, @dst) unless File.file? @dst
            raise IOError.new(:file_is_not_writable, nil, @dst) unless File.writable? @dst

            file << output
          end
        end
      end
    rescue StandardError => e
      raise IOError.new(:error_writing_file, e, @dst)
    end

    # run cleanup scripts
    cleanup template
  end

  def pandoc(input, options, src_dir)
    absolute_dst = File.expand_path @dst
    Dir.chdir(src_dir) do
      Pandocomatic::LOG.debug "     #  Changing directory to '#{src_dir}'"
      converter = Paru::Pandoc.new
      options.each do |option, value|
        # Options come from a YAML string. In YAML, properties without a value get value nil.
        # Interpret these empty properties as "skip this property"
        next if value.nil?

        if PANDOC_OPTIONS_WITH_PATH.include? option
          executable = option == 'filter'
          value = if value.is_a? Array
                    value.map { |v| Path.update_path(@config, v, absolute_dst, check_executable: executable) }
                  else
                    Path.update_path(@config, value, @dst, check_executable: executable)
                  end
        end

        # There is no "pdf" output format; change it to latex but keep the
        # extension.
        value = determine_output_for_pdf(options) if (option == 'to') && (value == 'pdf')

        begin
          # Pandoc multi-word options can have the multiple words separated by
          # both underscore (_) and dash (-).
          option = option.gsub '-', '_'
          converter.send option, value unless
                  (option == 'output') ||
                  (option == 'use_extension') ||
                  (option == 'rename')
          # don't let pandoc write the output to enable postprocessing
        rescue StandardError
          Pandocomatic::LOG.warn "WARNING: The pandoc option '#{option}'"
          " (with value '#{value}') is not recognized by paru. This option is skipped."
        end
      end

      converter.send 'output', absolute_dst if use_output_option absolute_dst

      begin
        Pandocomatic::LOG.debug '     #  Running pandoc'
        Pandocomatic::LOG.debug "     |  #{Pandocomatic::LOG.indent(converter.to_command, 43)}"
        converter << input
      rescue Paru::Error => e
        raise PandocError.new(:error_running_pandoc, e, input)
      end
    end
  end

  # Preprocess the input
  #
  # @param input [String] the input to preprocess
  # @param template [Template] template
  #
  # @return [String] the generated output
  def preprocess(input, template)
    process input, Template::PREPROCESSORS, template
  end

  # Postprocess the input
  #
  # @param input [String] the input to postprocess
  # @param template [Template] template
  #
  # @return [String] the generated output
  def postprocess(input, template)
    process input, Template::POSTPROCESSORS, template
  end

  # Run setup scripts
  #
  # @param template [Template] template
  def setup(template)
    process '', Template::SETUP, template
  end

  # Run cleanup scripts
  #
  # @param template [Template] template
  def cleanup(template)
    process '', Template::CLEANUP, template
  end

  # Run the input string through a list of filters called processors. There
  # are various types: preprocessors and postprocessors, setup and
  # cleanup, and rename
  def process(input, type, template)
    if template.send "#{type}?"
      processors = template.send type
      Pandocomatic::LOG.debug "     #  Running #{type}:" unless processors.empty?
      output = input
      processors.each do |processor|
        script = if Path.local_path? processor
                   processor
                 else
                   Path.update_path(@config, processor, @dst, check_executable: true)
                 end

        command, *parameters = script.shellsplit # split on spaces unless it is preceded by a backslash

        unless File.exist? command
          command = Path.which(command)
          script = "#{command} #{parameters.join(' ')}"

          raise ProcessorError.new(:script_does_not_exist, nil, command) if command.nil?
        end

        raise ProcessorError.new(:script_is_not_executable, nil, command) unless File.executable? command

        begin
          Pandocomatic::LOG.debug "     |  #{script}"
          output = Processor.run(script, output)
        rescue StandardError => e
          ProcessorError.new(:error_processing_script, e, [script, @src])
        end
      end
      output
    else
      input
    end
  end

  def use_output_option(dst)
    OUTPUT_FORMATS.include?(File.extname(dst).slice(1..-1))
  end

  # Pandoc version 2 supports multiple pdf engines. Determine which
  # to use given the options.
  #
  # @param options [Hash] the options to a paru pandoc converter
  # @return [String] the output format for the pdf engine to use.
  def determine_output_for_pdf(options)
    if options.key? 'pdf-engine'
      engine = options['pdf-engine']
      case engine
      when 'context'
        'context'
      when 'pdfroff'
        'ms'
      when 'wkhtmltopdf', 'weasyprint', 'prince'
        'html'
      else
        'latex'
      end
    else
      # According to pandoc's manual, the default is LaTeX
      'latex'
    end
  end
end

Instance Method Details

#runObject

Execute this ConvertFileCommand



113
114
115
# File 'lib/pandocomatic/command/convert_file_command.rb', line 113

def run
  convert_file
end

#to_sString

Create a string representation of this ConvertFileCommand

Returns:

  • (String)


120
121
122
123
124
125
126
127
128
# File 'lib/pandocomatic/command/convert_file_command.rb', line 120

def to_s
  str = "convert #{File.basename @src} #{"-> #{File.basename @dst}" unless @dst.nil?}"
  unless @metadata.unique?
    str += "\n\t encountered multiple YAML metadata blocks with a pandocomatic property. " \
           'Only the pandocomatic property in the first YAML metadata block is being used; ' \
           'the others are discarded.'
  end
  str
end