pandocomatic 0.2.7.8 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/lib/pandocomatic/cli.rb +81 -64
  3. data/lib/pandocomatic/command/command.rb +37 -35
  4. data/lib/pandocomatic/command/convert_dir_command.rb +44 -46
  5. data/lib/pandocomatic/command/convert_file_command.rb +314 -286
  6. data/lib/pandocomatic/command/convert_file_multiple_command.rb +56 -53
  7. data/lib/pandocomatic/command/convert_list_command.rb +31 -34
  8. data/lib/pandocomatic/command/copy_file_command.rb +14 -15
  9. data/lib/pandocomatic/command/create_link_command.rb +24 -27
  10. data/lib/pandocomatic/command/skip_command.rb +12 -15
  11. data/lib/pandocomatic/configuration.rb +682 -867
  12. data/lib/pandocomatic/default_configuration.yaml +4 -0
  13. data/lib/pandocomatic/error/cli_error.rb +30 -26
  14. data/lib/pandocomatic/error/configuration_error.rb +10 -9
  15. data/lib/pandocomatic/error/io_error.rb +13 -13
  16. data/lib/pandocomatic/error/pandoc_error.rb +10 -9
  17. data/lib/pandocomatic/error/pandocomatic_error.rb +15 -14
  18. data/lib/pandocomatic/error/processor_error.rb +9 -9
  19. data/lib/pandocomatic/error/template_error.rb +50 -0
  20. data/lib/pandocomatic/input.rb +53 -54
  21. data/lib/pandocomatic/multiple_files_input.rb +79 -72
  22. data/lib/pandocomatic/output.rb +29 -0
  23. data/lib/pandocomatic/pandoc_metadata.rb +193 -181
  24. data/lib/pandocomatic/pandocomatic.rb +101 -97
  25. data/lib/pandocomatic/pandocomatic_yaml.rb +70 -0
  26. data/lib/pandocomatic/path.rb +171 -0
  27. data/lib/pandocomatic/printer/command_printer.rb +7 -5
  28. data/lib/pandocomatic/printer/configuration_errors_printer.rb +7 -6
  29. data/lib/pandocomatic/printer/error_printer.rb +12 -7
  30. data/lib/pandocomatic/printer/finish_printer.rb +11 -10
  31. data/lib/pandocomatic/printer/help_printer.rb +8 -6
  32. data/lib/pandocomatic/printer/printer.rb +34 -34
  33. data/lib/pandocomatic/printer/summary_printer.rb +39 -33
  34. data/lib/pandocomatic/printer/version_printer.rb +8 -8
  35. data/lib/pandocomatic/printer/views/cli_error.txt +5 -0
  36. data/lib/pandocomatic/printer/views/configuration_error.txt +2 -1
  37. data/lib/pandocomatic/printer/views/error.txt +1 -1
  38. data/lib/pandocomatic/printer/views/finish.txt +1 -1
  39. data/lib/pandocomatic/printer/views/help.txt +27 -15
  40. data/lib/pandocomatic/printer/views/summary.txt +7 -1
  41. data/lib/pandocomatic/printer/views/template_error.txt +1 -0
  42. data/lib/pandocomatic/printer/views/version.txt +3 -3
  43. data/lib/pandocomatic/printer/views/warning.txt +1 -1
  44. data/lib/pandocomatic/printer/warning_printer.rb +21 -19
  45. data/lib/pandocomatic/processor.rb +28 -28
  46. data/lib/pandocomatic/processors/fileinfo_preprocessor.rb +35 -30
  47. data/lib/pandocomatic/processors/metadata_preprocessor.rb +23 -22
  48. data/lib/pandocomatic/template.rb +244 -0
  49. data/lib/pandocomatic/warning.rb +24 -25
  50. metadata +32 -12
@@ -1,951 +1,766 @@
1
+ # frozen_string_literal: true
2
+
1
3
  #--
2
- # Copyright 2014—2019 Huub de Beer <Huub@heerdebeer.org>
3
- #
4
+ # Copyright 2014—2022 Huub de Beer <Huub@heerdebeer.org>
5
+ #
4
6
  # This file is part of pandocomatic.
5
- #
7
+ #
6
8
  # Pandocomatic is free software: you can redistribute it and/or modify
7
9
  # it under the terms of the GNU General Public License as published by the
8
10
  # Free Software Foundation, either version 3 of the License, or (at your
9
11
  # option) any later version.
10
- #
12
+ #
11
13
  # Pandocomatic is distributed in the hope that it will be useful, but
12
14
  # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13
15
  # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14
16
  # for more details.
15
- #
17
+ #
16
18
  # You should have received a copy of the GNU General Public License along
17
19
  # with pandocomatic. If not, see <http://www.gnu.org/licenses/>.
18
20
  #++
19
21
  module Pandocomatic
20
-
21
- require 'yaml'
22
- require 'paru/pandoc'
23
-
24
- require_relative './error/configuration_error.rb'
25
- require_relative './command/command.rb'
26
- require_relative './input.rb'
27
- require_relative './multiple_files_input.rb'
28
-
29
- # The default configuration for pandocomatic is read from
30
- # default_configuration.yaml.
31
- DEFAULT_CONFIG = YAML.load_file File.join(__dir__, 'default_configuration.yaml')
32
-
33
- # Maps pandoc output formats to their conventional default extension.
34
- DEFAULT_EXTENSION = {
35
- 'native' => 'hs',
36
- 'plain' => 'txt',
37
- 'markdown' => 'md',
38
- 'markdown_strict' => 'md',
39
- 'markdown_phpextra' => 'md',
40
- 'markdown_mmd' => 'md',
41
- 'gfm' => 'md',
42
- 'commonmark' => 'md',
43
- 'html4' => 'html',
44
- 'html5' => 'html',
45
- 'latex' => 'tex',
46
- 'beamer' => 'tex',
47
- 'context' => 'tex',
48
- 'docbook4' => 'docbook',
49
- 'docbook5' => 'docbook',
50
- 'opendocument' => 'odt',
51
- 'epub2' => 'epub',
52
- 'epub3' => 'epub',
53
- 'asciidoc' => 'adoc',
54
- 'slidy' => 'html',
55
- 'slideous' => 'html',
56
- 'dzslides' => 'html',
57
- 'revealjs' => 'html',
58
- 's5' => 'html',
59
- 'bibtex' => 'bib',
60
- 'biblatex' => 'bib'
61
- }
62
-
63
- # Indicator for paths that should be treated as "relative to the root
64
- # path". These paths start with this ROOT_PATH_INDICATOR.
65
- ROOT_PATH_INDICATOR = "$ROOT$"
66
-
67
- # A Configuration object models a pandocomatic configuration.
68
- class Configuration
69
-
70
- attr_reader :input
71
-
72
- # Pandocomatic's default configuration file
73
- CONFIG_FILE = 'pandocomatic.yaml'
74
-
75
- # Create a new Configuration instance based on the command-line options
76
- def initialize options, input
77
- @options = options
78
- @data_dir = determine_data_dir options
79
- config_file = determine_config_file(options, @data_dir)
80
-
81
- load config_file unless config_file.nil? or config_file.empty?
82
-
83
- @input = if input.nil? or input.empty? then
84
- nil
85
- elsif 1 < input.size then
86
- MultipleFilesInput.new(input, self)
87
- else
88
- Input.new(input)
89
- end
90
-
91
- @output = if output? then
92
- options[:output]
93
- elsif @input.is_a? Input then
94
- @input.base
95
- else
96
- nil
97
- end
98
-
99
- @root_path = determine_root_path options
100
-
101
- # Extend the command classes by setting the source tree root
102
- # directory, and the options quiet and dry-run, which are used when
103
- # executing a command: if dry-run the command is not actually
104
- # executed and if quiet the command is not printed to STDOUT
105
- Command.reset(self)
106
- end
107
-
108
- # Read a configuration file and create a pandocomatic configuration object
109
- #
110
- # @param [String] filename Path to the configuration yaml file
111
- # @return [Configuration] a pandocomatic configuration object
112
- def load(filename)
113
- begin
114
- path = File.absolute_path filename
115
- settings = YAML.load_file path
116
- if settings['settings'] and settings['settings']['data-dir'] then
117
- data_dir = settings['settings']['data-dir']
118
- src_dir = File.dirname filename
119
- if data_dir.start_with? '.' then
120
- @data_dir = File.absolute_path data_dir, src_dir
121
- else
122
- @data_dir = data_dir
123
- end
124
- end
125
- rescue StandardError => e
126
- raise ConfigurationError.new(:unable_to_load_config_file, e, filename)
127
- end
128
-
129
- # hidden files will always be skipped, as will pandocomatic
130
- # configuration files, unless explicitly set to not skip via the
131
- # "unskip" option
132
-
133
- @settings = {
134
- 'skip' => ['.*', 'pandocomatic.yaml'],
135
- 'recursive' => true,
136
- 'follow-links' => false,
137
- 'match-files' => 'first'
138
- }
139
-
140
- @templates = {}
141
- @convert_patterns = {}
142
-
143
- configure settings
144
- end
145
-
146
- # Update this configuration with a configuration file
147
- #
148
- # @param [String] filename path to the configuration file
149
- #
150
- # @return [Configuration] a new configuration
151
- def reconfigure(filename)
152
- begin
153
- settings = YAML.load_file filename
154
- new_config = Marshal.load(Marshal.dump(self))
155
- new_config.configure settings
156
- new_config
157
- rescue StandardError => e
158
- raise ConfigurationError.new(:unable_to_load_config_file, e, filename)
159
- end
160
- end
161
-
162
- # Configure pandocomatic based on a settings Hash
163
- #
164
- # @param settings [Hash] a settings Hash to mixin in this
165
- # Configuration.
166
- def configure(settings)
167
- reset_settings settings['settings'] if settings.has_key? 'settings'
168
- if settings.has_key? 'templates' then
169
- settings['templates'].each do |name, template|
170
- full_template = {
171
- 'extends' => [],
172
- 'glob' => [],
173
- 'setup' => [],
174
- 'preprocessors' => [],
175
- 'metadata' => {},
176
- 'pandoc' => {},
177
- 'postprocessors' => [],
178
- 'cleanup' => []
179
- }
180
-
181
- reset_template name, full_template.merge(template)
22
+ require 'paru/pandoc'
23
+
24
+ require_relative './error/configuration_error'
25
+ require_relative './command/command'
26
+ require_relative './input'
27
+ require_relative './multiple_files_input'
28
+ require_relative './pandocomatic_yaml'
29
+ require_relative './path'
30
+ require_relative './template'
31
+
32
+ # The default configuration for pandocomatic is read from
33
+ # default_configuration.yaml.
34
+ DEFAULT_CONFIG = PandocomaticYAML.load_file File.join(__dir__, 'default_configuration.yaml')
35
+
36
+ # rubocop:disable Style/MutableConstant
37
+
38
+ # The default settings for pandocomatic:
39
+ # hidden files will always be skipped, as will pandocomatic
40
+ # configuration files, unless explicitly set to not skip via the
41
+ # "unskip" option
42
+ DEFAULT_SETTINGS = {
43
+ 'skip' => ['.*', 'pandocomatic.yaml'],
44
+ 'recursive' => true,
45
+ 'follow-links' => false,
46
+ 'match-files' => 'first'
47
+ }
48
+ # rubocop:enable Style/MutableConstant
49
+
50
+ # Maps pandoc output formats to their conventional default extension.
51
+ # Updated and in order of `pandoc --list-output-formats`.
52
+ DEFAULT_EXTENSION = {
53
+ 'asciidoc' => 'adoc',
54
+ 'asciidoctor' => 'adoc',
55
+ 'beamer' => 'tex',
56
+ 'bibtex' => 'bib',
57
+ 'biblatex' => 'bib',
58
+ 'commonmark' => 'md',
59
+ 'context' => 'tex',
60
+ 'csljson' => 'json',
61
+ 'docbook' => 'docbook',
62
+ 'docbook4' => 'docbook',
63
+ 'docbook5' => 'docbook',
64
+ 'docx' => 'docx',
65
+ 'dokuwiki' => 'txt',
66
+ 'dzslides' => 'html',
67
+ 'epub' => 'epub',
68
+ 'epub2' => 'epub',
69
+ 'epub3' => 'epub',
70
+ 'fb2' => 'fb2',
71
+ 'gfm' => 'md',
72
+ 'haddock' => 'hs',
73
+ 'html' => 'html',
74
+ 'html4' => 'html',
75
+ 'html5' => 'html',
76
+ 'icml' => 'icml',
77
+ 'ipynb' => 'ipynb',
78
+ 'jats' => 'jats',
79
+ 'jats_archiving' => 'jats',
80
+ 'jats_articleauthoring' => 'jats',
81
+ 'jats_publishing' => 'jats',
82
+ 'jira' => 'jira',
83
+ 'json' => 'json',
84
+ 'latex' => 'tex',
85
+ 'man' => 'man',
86
+ 'markdown' => 'md',
87
+ 'markdown_github' => 'md',
88
+ 'markdown_mmd' => 'md',
89
+ 'markdown_phpextra' => 'md',
90
+ 'markdown_strict' => 'md',
91
+ 'media_wiki' => 'mediawiki',
92
+ 'ms' => 'ms',
93
+ 'muse' => 'muse',
94
+ 'native' => 'hs',
95
+ 'odt' => 'odt',
96
+ 'opendocument' => 'odt',
97
+ 'opml' => 'opml',
98
+ 'org' => 'org',
99
+ 'pdf' => 'pdf',
100
+ 'plain' => 'txt',
101
+ 'pptx' => 'pptx',
102
+ 'revealjs' => 'html',
103
+ 'rst' => 'rst',
104
+ 's5' => 'html',
105
+ 'slideous' => 'html',
106
+ 'slidy' => 'html',
107
+ 'tei' => 'tei',
108
+ 'texinfo' => 'texi',
109
+ 'textile' => 'textile',
110
+ 'xwiki' => 'xwiki',
111
+ 'zimwiki' => 'zimwiki'
112
+ }.freeze
113
+
114
+ # rubocop:disable Metrics
115
+
116
+ # Configuration models a pandocomatic configuration.
117
+ class Configuration
118
+ attr_reader :input, :config_files, :data_dir, :root_path
119
+
120
+ # Pandocomatic's default configuration file
121
+ CONFIG_FILE = 'pandocomatic.yaml'
122
+
123
+ # Create a new Configuration instance based on the command-line options
124
+ def initialize(options, input)
125
+ data_dirs = determine_data_dirs options
126
+ @options = options
127
+ @data_dir = data_dirs.first
128
+ @settings = DEFAULT_SETTINGS
129
+ @templates = {}
130
+ @convert_patterns = {}
131
+
132
+ load_configuration_hierarchy options, data_dirs
133
+
134
+ @input = if input.nil? || input.empty?
135
+ nil
136
+ elsif input.size > 1
137
+ MultipleFilesInput.new(input, self)
138
+ else
139
+ Input.new(input)
140
+ end
141
+
142
+ @output = if output?
143
+ options[:output]
144
+ elsif to_stdout? options
145
+ Tempfile.new(@input.base)
146
+ elsif @input.is_a? Input
147
+ @input.base
182
148
  end
183
- end
184
- end
185
-
186
- # Convert this Configuration to a String
187
- #
188
- # @return [String]
189
- def to_s()
190
- marshal_dump
191
- end
192
-
193
- # Is the dry run CLI option given?
194
- #
195
- # @return [Boolean]
196
- def dry_run?()
197
- @options[:dry_run_given] and @options[:dry_run]
198
- end
199
-
200
- # Is the quiet CLI option given?
201
- #
202
- # @return [Boolean]
203
- def quiet?()
204
- @options[:quiet_given] and @options[:quiet]
205
- end
206
-
207
- # Is the debug CLI option given?
208
- #
209
- # @return [Boolean]
210
- def debug?()
211
- @options[:debug_given] and @options[:debug]
212
- end
213
-
214
- # Is the modified only CLI option given?
215
- #
216
- # @return [Boolean]
217
- def modified_only?()
218
- @options[:modified_only_given] and @options[:modified_only]
219
- end
220
-
221
- # Is the version CLI option given?
222
- #
223
- # @return [Boolean]
224
- def show_version?()
225
- @options[:version_given]
226
- end
227
-
228
- # Is the help CLI option given?
229
- #
230
- # @return [Boolean]
231
- def show_help?()
232
- @options[:help_given]
233
- end
234
-
235
- # Is the data dir CLI option given?
236
- #
237
- # @return [Boolean]
238
- def data_dir?()
239
- @options[:data_dir_given]
240
- end
241
149
 
242
- # Is the root path CLI option given?
243
- #
244
- # @return [Boolean]
245
- def root_path?()
246
- @options[:root_path_given]
247
- end
248
-
249
- # Is the config CLI option given?
250
- #
251
- # @return [Boolean]
252
- def config?()
253
- @options[:config_given]
254
- end
255
-
256
- # Is the output CLI option given and can that output be used?
257
- #
258
- # @return [Boolean]
259
- def output?()
260
- not @options.nil? and @options[:output_given] and @options[:output]
261
- end
150
+ @root_path = Path.determine_root_path options
262
151
 
263
- # Get the output file name
264
- #
265
- # @return [String]
266
- def output()
267
- @output
268
- end
269
-
270
- # Get the source root directory
271
- #
272
- # @return [String]
273
- def src_root()
274
- if @input.nil? then nil else @input.absolute_path end
275
- end
152
+ # Extend the command classes by setting the source tree root
153
+ # directory, and the options quiet and dry-run, which are used when
154
+ # executing a command: if dry-run the command is not actually
155
+ # executed and if quiet the command is not printed to STDOUT
156
+ Command.reset(self)
157
+ end
276
158
 
277
- # Have input CLI options be given?
278
- def input?()
279
- @options[:input_given]
159
+ # Read a configuration file and create a pandocomatic configuration object
160
+ #
161
+ # @param [String] filename Path to the configuration yaml file
162
+ # @return [Configuration] a pandocomatic configuration object
163
+ def load(filename)
164
+ begin
165
+ path = File.absolute_path filename
166
+ settings = PandocomaticYAML.load_file path
167
+ if settings['settings'] && settings['settings']['data-dir']
168
+ data_dir = settings['settings']['data-dir']
169
+ src_dir = File.dirname filename
170
+ @data_dir = if data_dir.start_with? '.'
171
+ File.absolute_path data_dir, src_dir
172
+ else
173
+ data_dir
174
+ end
280
175
  end
176
+ rescue StandardError => e
177
+ raise ConfigurationError.new(:unable_to_load_config_file, e, filename)
178
+ end
281
179
 
282
- # Get the input file name
283
- #
284
- # @return [String]
285
- def input_file()
286
- if @input.nil? then
287
- nil
288
- else
289
- @input.name
290
- end
291
- end
180
+ configure settings, filename
181
+ end
292
182
 
293
- # Is this Configuration for converting directories?
294
- #
295
- # @return [Boolean]
296
- def directory?()
297
- not @input.nil? and @input.directory?
298
- end
183
+ # Update this configuration with a configuration file and return a new
184
+ # configuration
185
+ #
186
+ # @param [String] filename path to the configuration file
187
+ #
188
+ # @return [Configuration] a new configuration
189
+ def reconfigure(filename)
190
+ settings = PandocomaticYAML.load_file filename
191
+ new_config = Marshal.load(Marshal.dump(self))
192
+ new_config.configure settings, filename
193
+ new_config
194
+ rescue StandardError => e
195
+ raise ConfigurationError.new(:unable_to_load_config_file, e, filename)
196
+ end
299
197
 
300
- # Clean up this configuration. This will remove temporary files
301
- # created for the conversion process guided by this Configuration.
302
- def clean_up!()
303
- # If a temporary file has been created while concatenating
304
- # multiple input files, ensure it is removed.
305
- if @input.is_a? MultipleFilesInput then
306
- @input.destroy!
307
- end
308
- end
198
+ # Configure pandocomatic based on a settings Hash
199
+ #
200
+ # @param settings [Hash] a settings Hash to mixin in this
201
+ # @param path [String] the configuration's path or filename
202
+ # Configuration.
203
+ def configure(settings, path)
204
+ reset_settings settings['settings'] if settings.key? 'settings'
309
205
 
310
- # Should the source file be skipped given this Configuration?
311
- #
312
- # @param src [String] path to a source file
313
- # @return [Boolean] True if this source file matches the pattern in
314
- # the 'skip' setting, false otherwise.
315
- def skip?(src)
316
- if @settings.has_key? 'skip' then
317
- @settings['skip'].any? {|glob| File.fnmatch glob, File.basename(src)}
318
- else
319
- false
320
- end
321
- end
206
+ return unless settings.key? 'templates'
322
207
 
323
- # Should the source file be converted given this Configuration?
324
- #
325
- # @param src [String] True if this source file matches the 'glob'
326
- # patterns in a template, false otherwise.
327
- def convert?(src)
328
- @convert_patterns.values.flatten.any? {|glob| File.fnmatch glob, File.basename(src)}
329
- end
208
+ settings['templates'].each do |name, template|
209
+ reset_template Template.new(name, template, path)
210
+ end
211
+ end
330
212
 
331
- # Should pandocomatic be run recursively given this Configuration?
332
- #
333
- # @return [Boolean] True if the setting 'recursive' is true, false
334
- # otherwise
335
- def recursive?()
336
- @settings.has_key? 'recursive' and @settings['recursive']
337
- end
213
+ # Convert this Configuration to a String
214
+ #
215
+ # @return [String]
216
+ def to_s
217
+ marshal_dump
218
+ end
338
219
 
339
- # Should pandocomatic follow symbolic links given this Configuration?
340
- #
341
- # @return [Boolean] True if the setting 'follow_links' is true, false
342
- # otherwise
343
- def follow_links?()
344
- @settings.has_key? 'follow_links' and @settings['follow_links']
345
- end
220
+ # Is the dry run CLI option given?
221
+ #
222
+ # @return [Boolean]
223
+ def dry_run?
224
+ @options[:dry_run_given] and @options[:dry_run]
225
+ end
346
226
 
347
- # Should pandocomatic convert a file with all matching templates or
348
- # only with the first matching template? Note. A 'use-template'
349
- # statement in a document will overrule this setting.
350
- #
351
- # @return [Boolean] True if the setting 'match-files' is 'all', false
352
- # otherwise.
353
- def match_all_templates?()
354
- @settings.has_key? 'match-files' and 'all' == @settings['match-files']
355
- end
227
+ # Is the stdout CLI option given?
228
+ #
229
+ # @return [Boolean]
230
+ def stdout?
231
+ !@options.nil? and @options[:stdout_given] and @options[:stdout]
232
+ end
356
233
 
357
- # Should pandocomatic convert a file with the first matching templates
358
- # or with all matching templates? Note. Multiple 'use-template'
359
- # statements in a document will overrule this setting.
360
- #
361
- # @return [Boolean] True if the setting 'match-files' is 'first', false
362
- # otherwise.
363
- def match_first_template?()
364
- @settings.has_key? 'match-files' and 'first' == @settings['match-files']
365
- end
234
+ # Is the verbose CLI option given?
235
+ #
236
+ # @return [Boolean]
237
+ def verbose?
238
+ @options[:verbose_given] and @options[:verbose]
239
+ end
366
240
 
367
- # Set the extension of the destination file given this Confguration,
368
- # template, and metadata
369
- #
370
- # @param dst [String] path to a destination file
371
- # @param template_name [String] the name of the template used to
372
- # convert to destination
373
- # @param metadata [PandocMetadata] the metadata in the source file
374
- def set_extension(dst, template_name, metadata)
375
- dir = File.dirname dst
376
- ext = File.extname dst
377
- basename = File.basename dst, ext
378
- File.join dir, "#{basename}.#{find_extension(dst, template_name, metadata)}"
379
- end
241
+ # Is the debug CLI option given?
242
+ #
243
+ # @return [Boolean]
244
+ def debug?
245
+ @options[:debug_given] and @options[:debug]
246
+ end
380
247
 
381
- # Set the destination file given this Confguration,
382
- # template, and metadata
383
- #
384
- # @param dst [String] path to a destination file
385
- # @param template_name [String] the name of the template used to
386
- # convert to destination
387
- # @param metadata [PandocMetadata] the metadata in the source file
388
- def set_destination(dst, template_name, metadata)
389
- dir = File.dirname dst
390
-
391
- # Use the output option when set.
392
- determine_output_in_pandoc = lambda do |pandoc|
393
- if pandoc.has_key? "output"
394
- output = pandoc["output"]
395
- if not output.start_with? "/"
396
- # Put it relative to the current directory
397
- output = File.join dir, output
398
- end
399
- output
400
- else
401
- nil
402
- end
403
- end
404
-
405
- # Output options in pandoc property have precedence
406
- destination = determine_output_in_pandoc.call metadata.pandoc_options
407
- rename_script = metadata.pandoc_options["rename"]
408
-
409
- # Output option in template's pandoc property is next
410
- if destination.nil? and not template_name.nil? and not template_name.empty? then
411
- if @templates[template_name].has_key? "pandoc" and not @templates[template_name]["pandoc"].nil?
412
- pandoc = @templates[template_name]["pandoc"]
413
- destination = determine_output_in_pandoc.call pandoc
414
- rename_script ||= pandoc["rename"]
415
- end
416
- end
248
+ # Run pandocomatic in quiet mode?
249
+ #
250
+ # @return [Boolean]
251
+ def quiet?
252
+ [verbose?, debug?, dry_run?].none?
253
+ end
417
254
 
418
- # Else fall back to taking the input file as output file with the
419
- # extension updated to the output format
420
- if destination.nil?
421
- destination = set_extension dst, template_name, metadata
255
+ # Is the modified only CLI option given?
256
+ #
257
+ # @return [Boolean]
258
+ def modified_only?
259
+ @options[:modified_only_given] and @options[:modified_only]
260
+ end
422
261
 
423
- if not rename_script.nil? then
424
- destination = rename_destination(rename_script, destination)
425
- end
426
- end
262
+ # Is the version CLI option given?
263
+ #
264
+ # @return [Boolean]
265
+ def show_version?
266
+ @options[:version_given]
267
+ end
427
268
 
428
- # If there is a single file input without output specified, set
429
- # the output now that we know what the output filename is.
430
- @output = destination.delete_prefix "./" if not output?
431
-
432
- destination
433
- end
269
+ # Is the help CLI option given?
270
+ #
271
+ # @return [Boolean]
272
+ def show_help?
273
+ @options[:help_given]
274
+ end
434
275
 
435
- # Find the extension of the destination file given this Confguration,
436
- # template, and metadata
437
- #
438
- # @param dst [String] path to a destination file
439
- # @param template_name [String] the name of the template used to
440
- # convert to destination
441
- # @param metadata [PandocMetadata] the metadata in the source file
442
- #
443
- # @return [String] the extension to use for the destination file
444
- def find_extension(dst, template_name, metadata)
445
- extension = "html"
446
-
447
- # Pandoc supports enabling / disabling extensions
448
- # using +EXTENSION and -EXTENSION
449
- strip_extensions = lambda{|format| format.split(/[+-]/).first}
450
- use_extension = lambda do |pandoc|
451
- if pandoc.has_key? "use-extension"
452
- pandoc["use-extension"]
453
- else
454
- nil
455
- end
456
- end
457
-
458
- if template_name.nil? or template_name.empty? then
459
- ext = use_extension.call metadata.pandoc_options
460
- if not ext.nil?
461
- extension = ext
462
- elsif metadata.pandoc_options.has_key? "to"
463
- extension = strip_extensions.call(metadata.pandoc_options["to"])
464
- end
465
- else
466
- if @templates[template_name].has_key? "pandoc" and not @templates[template_name]["pandoc"].nil?
467
- pandoc = @templates[template_name]["pandoc"]
468
- ext = use_extension.call pandoc
469
-
470
- if not ext.nil?
471
- extension = ext
472
- elsif pandoc.has_key? "to"
473
- extension = strip_extensions.call(pandoc["to"])
474
- end
475
- end
476
- end
276
+ # Is the data dir CLI option given?
277
+ #
278
+ # @return [Boolean]
279
+ def data_dir?
280
+ @options[:data_dir_given]
281
+ end
477
282
 
478
- extension = DEFAULT_EXTENSION[extension] || extension
479
- extension
480
- end
283
+ # Is the root path CLI option given?
284
+ #
285
+ # @return [Boolean]
286
+ def root_path?
287
+ @options[:root_path_given]
288
+ end
481
289
 
482
- def is_markdown_file?(filename)
483
- if filename.nil? then
484
- false
485
- else
486
- ext = File.extname(filename).delete_prefix(".");
487
- "markdown" == DEFAULT_EXTENSION.key(ext)
488
- end
489
- end
290
+ # Is the config CLI option given?
291
+ #
292
+ # @return [Boolean]
293
+ def config?
294
+ @options[:config_given]
295
+ end
490
296
 
491
- # Is there a template with template_name in this Configuration?
492
- #
493
- # @param template_name [String] a template's name
494
- #
495
- # @return [Boolean] True if there is a template with name equal to
496
- # template_name in this Configuration
497
- def has_template?(template_name)
498
- @templates.has_key? template_name
499
- end
297
+ # Is the output CLI option given and can that output be used?
298
+ #
299
+ # @return [Boolean]
300
+ def output?
301
+ !@options.nil? and @options[:output_given] and @options[:output]
302
+ end
500
303
 
501
- # Get the template with template_name from this Configuration
502
- #
503
- # @param template_name [String] a template's name
504
- #
505
- # @return [Hash] The template with template_name.
506
- def get_template(template_name)
507
- @templates[template_name]
508
- end
304
+ # Get the output file name
305
+ #
306
+ # @return [String]
307
+ attr_reader :output
509
308
 
510
- # Determine the template to use with this source document given this
511
- # Configuration.
512
- #
513
- # @param src [String] path to the source document
514
- # @return [String] the template's name to use
515
- def determine_template(src)
516
- @convert_patterns.select do |template_name, globs|
517
- globs.any? {|glob| File.fnmatch glob, File.basename(src)}
518
- end.keys.first
519
- end
309
+ # Get the source root directory
310
+ #
311
+ # @return [String]
312
+ def src_root
313
+ @input.nil? ? nil : @input.absolute_path
314
+ end
520
315
 
521
- # Determine the templates to use with this source document given this
522
- # Configuration.
523
- #
524
- # @param src [String] path to the source document
525
- # @return [Array[String]] the template's name to use
526
- def determine_templates(src)
527
- matches = @convert_patterns.select do |template_name, globs|
528
- globs.any? {|glob| File.fnmatch glob, File.basename(src)}
529
- end.keys
530
-
531
- if matches.empty?
532
- []
533
- elsif match_all_templates?
534
- matches
535
- else
536
- [matches.first]
537
- end
538
- end
316
+ # Have input CLI options be given?
317
+ def input?
318
+ @options[:input_given]
319
+ end
539
320
 
540
- # Update the path to an executable processor or executor given this
541
- # Configuration
542
- #
543
- # @param path [String] path to the executable
544
- # @param src_dir [String] the source directory from which pandocomatic
545
- # conversion process has been started
546
- # @param dst [String] the destination path
547
- # @param check_executable [Boolean = false] Should the executable be
548
- # verified to be executable? Defaults to false.
549
- #
550
- # @return [String] the updated path.
551
- def update_path(path, src_dir, dst = "", check_executable = false, output = false)
552
- updated_path = path
553
-
554
- if is_local_path? path
555
- # refers to a local dir; strip the './' before appending it to
556
- # the source directory as to prevent /some/path/./to/path
557
- updated_path = path[2..-1]
558
- elsif is_absolute_path? path
559
- updated_path = path
560
- elsif is_root_relative_path? path
561
- updated_path = make_path_root_relative path, dst, @root_path
562
- else
563
- if check_executable
564
- updated_path = Configuration.which path
565
- end
321
+ # Get the input file name
322
+ #
323
+ # @return [String]
324
+ def input_file
325
+ if @input.nil?
326
+ nil
327
+ else
328
+ @input.name
329
+ end
330
+ end
566
331
 
567
- if updated_path.nil? or not check_executable then
568
- # refers to data-dir
569
- updated_path = File.join @data_dir, path
570
- end
571
- end
332
+ # Is this Configuration for converting directories?
333
+ #
334
+ # @return [Boolean]
335
+ def directory?
336
+ !@input.nil? and @input.directory?
337
+ end
572
338
 
573
- updated_path
574
- end
339
+ # Clean up this configuration. This will remove temporary files
340
+ # created for the conversion process guided by this Configuration.
341
+ def clean_up!
342
+ # If a temporary file has been created while concatenating
343
+ # multiple input files, ensure it is removed.
344
+ @input.destroy! if @input.is_a? MultipleFilesInput
345
+ end
575
346
 
576
- # Extend the current value with the parent value. Depending on the
577
- # value and type of the current and parent values, the extension
578
- # differs.
579
- #
580
- # For simple values, the current value takes precedence over the
581
- # parent value
582
- #
583
- # For Hash values, each parent value's property is extended as well
584
- #
585
- # For Arrays, the current overwrites and adds to parent value's items
586
- # unless the current value is a Hash with a 'remove' and 'add'
587
- # property. Then the 'add' items are added to the parent value and the
588
- # 'remove' items are removed from the parent value.
589
- #
590
- # @param current [Object] the current value
591
- # @param parent [Object] the parent value the current might extend
592
- # @return [Object] the extended value
593
- def self.extend_value(current, parent)
594
- if parent.nil?
595
- # If no parent value is specified, the current takes
596
- # precedence
597
- current
598
- else
599
- if current.nil?
600
- # Current nil removes value of parent; follows YAML spec.
601
- # Note. take care to actually remove this value from a
602
- # Hash. (Like it is done in the next case)
603
- nil
604
- else
605
- if parent.is_a? Hash
606
- if current.is_a? Hash
607
- # Mixin current and parent values
608
- parent.each_pair do |property, value|
609
- if current.has_key? property
610
- extended_value = Configuration.extend_value(current[property], value)
611
- if extended_value.nil?
612
- current.delete property
613
- else
614
- current[property] = extended_value
615
- end
616
- else
617
- current[property] = value
618
- end
619
- end
620
- end
621
- current
622
- elsif parent.is_a? Array
623
- if current.is_a? Hash
624
- if current.has_key? 'remove'
625
- to_remove = current['remove']
626
-
627
- if to_remove.is_a? Array
628
- parent.delete_if {|v| current['remove'].include? v}
629
- else
630
- parent.delete to_remove
631
- end
632
- end
633
-
634
- if current.has_key? 'add'
635
- to_add = current['add']
636
-
637
- if to_add.is_a? Array
638
- parent = current['add'].concat(parent).uniq
639
- else
640
- parent.push(to_add).uniq
641
- end
642
- end
643
-
644
- parent
645
- elsif current.is_a? Array
646
- # Just combine parent and current arrays, current
647
- # values take precedence
648
- current.concat(parent).uniq
649
- else
650
- # Unknown what to do, assuming current should take
651
- # precedence
652
- current
653
- end
654
- else
655
- # Simple values: current replaces parent
656
- current
657
- end
658
- end
659
- end
660
- end
347
+ # Should the source file be skipped given this Configuration?
348
+ #
349
+ # @param src [String] path to a source file
350
+ # @return [Boolean] True if this source file matches the pattern in
351
+ # the 'skip' setting, false otherwise.
352
+ def skip?(src)
353
+ if @settings.key? 'skip'
354
+ @settings['skip'].any? { |glob| File.fnmatch glob, File.basename(src) }
355
+ else
356
+ false
357
+ end
358
+ end
661
359
 
662
- def is_local_path?(path)
663
- if Gem.win_platform? then
664
- path.match("^\\.\\\\\.*$")
665
- else
666
- path.start_with? "./"
667
- end
668
- end
360
+ # Should the source file be converted given this Configuration?
361
+ #
362
+ # @param src [String] True if this source file matches the 'glob'
363
+ # patterns in a template, false otherwise.
364
+ def convert?(src)
365
+ @convert_patterns.values.flatten.any? { |glob| File.fnmatch glob, File.basename(src) }
366
+ end
669
367
 
670
- private
671
-
672
- # Reset the settings for pandocomatic based on a new settings Hash
673
- #
674
- # @param settings [Hash] the new settings to use to reset the settings in
675
- # this Configuration with.
676
- def reset_settings(settings)
677
- settings.each do |setting, value|
678
- case setting
679
- when 'skip'
680
- @settings['skip'] = @settings['skip'].concat(value).uniq
681
- when 'data-dir'
682
- next # skip data-dir setting; is set once in initialization
683
- else
684
- @settings[setting] = value
685
- end
686
- end
687
- end
368
+ # Should pandocomatic be run recursively given this Configuration?
369
+ #
370
+ # @return [Boolean] True if the setting 'recursive' is true, false
371
+ # otherwise
372
+ def recursive?
373
+ @settings.key? 'recursive' and @settings['recursive']
374
+ end
688
375
 
689
- # Deep copy a template
690
- #
691
- # @param template [Hash] the template to clone
692
- # @return [Hash]
693
- def clone_template(template)
694
- Marshal.load(Marshal.dump(template))
695
- end
376
+ # Should pandocomatic follow symbolic links given this Configuration?
377
+ #
378
+ # @return [Boolean] True if the setting 'follow_links' is true, false
379
+ # otherwise
380
+ def follow_links?
381
+ @settings.key? 'follow_links' and @settings['follow_links']
382
+ end
696
383
 
697
- # Merge two templates
698
- #
699
- # @param base_template [Hash] the base template
700
- # @param mixin_template [Hash] the template to mixin into the base template
701
- # @return [Hash] the merged templates
702
- def merge(base_template, mixin_template)
703
- if mixin_template['extends'] and mixin_template['extends'].is_a? String
704
- mixin_template['extends'] = [mixin_template['extends']]
705
- end
706
-
707
- fields = [
708
- 'glob',
709
- 'metadata',
710
- 'setup',
711
- 'preprocessors',
712
- 'pandoc',
713
- 'postprocessors',
714
- 'cleanup'
715
- ]
716
-
717
- fields.each do |field|
718
- parent = base_template[field]
719
- current = mixin_template[field]
720
- extended_value = Configuration.extend_value current, parent
721
-
722
- if extended_value.nil?
723
- base_template.delete field
724
- else
725
- base_template[field] = extended_value
726
- end
727
- end
384
+ # Should pandocomatic convert a file with all matching templates or
385
+ # only with the first matching template? Note. A 'use-template'
386
+ # statement in a document will overrule this setting.
387
+ #
388
+ # @return [Boolean] True if the setting 'match-files' is 'all', false
389
+ # otherwise.
390
+ def match_all_templates?
391
+ @settings.key? 'match-files' and @settings['match-files'] == 'all'
392
+ end
728
393
 
729
- base_template
730
- end
394
+ # Should pandocomatic convert a file with the first matching templates
395
+ # or with all matching templates? Note. Multiple 'use-template'
396
+ # statements in a document will overrule this setting.
397
+ #
398
+ # @return [Boolean] True if the setting 'match-files' is 'first', false
399
+ # otherwise.
400
+ def match_first_template?
401
+ @settings.key? 'match-files' and @settings['match-files'] == 'first'
402
+ end
731
403
 
732
- # Resolve the templates the templates extends and mixes them in, in
733
- # order of occurrence.
734
- #
735
- # @param template [Hash] the template to extend
736
- # @return [Hash] the resolved template
737
- def extend_template(template)
738
- resolved_template = {};
739
- if template.has_key? 'extends' and not template['extends'].empty?
740
- to_extend = template['extends']
741
- to_extend = [to_extend] if to_extend.is_a? String
742
-
743
- to_extend.each do |name|
744
- if @templates.has_key? name
745
- merge resolved_template, clone_template(@templates[name])
746
- else
747
- warn "Cannot find template with name '#{name}'. Skipping this template while extending: '#{template.to_s}'."
748
- end
749
- end
404
+ # Set the extension of the destination file given this Confguration,
405
+ # template, and metadata
406
+ #
407
+ # @param dst [String] path to a destination file
408
+ # @param template_name [String] the name of the template used to
409
+ # convert to destination
410
+ # @param metadata [PandocMetadata] the metadata in the source file
411
+ def set_extension(dst, template_name, metadata)
412
+ dir = File.dirname dst
413
+ ext = File.extname dst
414
+ basename = File.basename dst, ext
415
+ File.join dir, "#{basename}.#{find_extension(template_name, metadata)}"
416
+ end
750
417
 
751
- resolved_template
752
- end
418
+ # Set the destination file given this Confguration,
419
+ # template, and metadata
420
+ #
421
+ # @param dst [String] path to a destination file
422
+ # @param template_name [String] the name of the template used to
423
+ # convert to destination
424
+ # @param metadata [PandocMetadata] the metadata in the source file
425
+ def set_destination(dst, template_name, metadata)
426
+ return dst if dst.is_a? Tempfile
427
+
428
+ dir = File.dirname dst
429
+
430
+ # Use the output option when set.
431
+ determine_output_in_pandoc = lambda do |pandoc|
432
+ if pandoc.key? 'output'
433
+ output = pandoc['output']
434
+ unless output.start_with? '/'
435
+ # Put it relative to the current directory
436
+ output = File.join dir, output
437
+ end
438
+ output
439
+ end
440
+ end
441
+
442
+ # Output options in pandoc property have precedence
443
+ destination = determine_output_in_pandoc.call metadata.pandoc_options
444
+ rename_script = metadata.pandoc_options['rename']
445
+
446
+ # Output option in template's pandoc property is next
447
+ if destination.nil? && !template_name.nil? && !template_name.empty? && @templates[template_name].pandoc?
448
+ pandoc = @templates[template_name].pandoc
449
+ destination = determine_output_in_pandoc.call pandoc
450
+ rename_script ||= pandoc['rename']
451
+ end
452
+
453
+ # Else fall back to taking the input file as output file with the
454
+ # extension updated to the output format
455
+ if destination.nil?
456
+ destination = set_extension dst, template_name, metadata
457
+
458
+ destination = rename_destination(rename_script, destination) unless rename_script.nil?
459
+ end
460
+
461
+ # If there is a single file input without output specified, set
462
+ # the output now that we know what the output filename is.
463
+ @output = destination.delete_prefix './' unless output?
464
+
465
+ destination
466
+ end
753
467
 
754
- merge resolved_template, template
755
- end
468
+ # Find the extension of the destination file given this Confguration,
469
+ # template, and metadata
470
+ #
471
+ # @param template_name [String] the name of the template used to
472
+ # convert to destination
473
+ # @param metadata [PandocMetadata] the metadata in the source file
474
+ #
475
+ # @return [String] the extension to use for the destination file
476
+ def find_extension(template_name, metadata)
477
+ extension = 'html'
478
+
479
+ # Pandoc supports enabling / disabling extensions
480
+ # using +EXTENSION and -EXTENSION
481
+ strip_extensions = ->(format) { format.split(/[+-]/).first }
482
+ use_extension = lambda do |pandoc|
483
+ pandoc['use-extension'] if pandoc.key? 'use-extension'
484
+ end
485
+
486
+ if template_name.nil? || template_name.empty?
487
+ ext = use_extension.call metadata.pandoc_options
488
+ if !ext.nil?
489
+ extension = ext
490
+ elsif metadata.pandoc_options.key? 'to'
491
+ extension = strip_extensions.call(metadata.pandoc_options['to'])
492
+ end
493
+ elsif @templates[template_name].pandoc?
494
+ pandoc = @templates[template_name].pandoc
495
+ ext = use_extension.call pandoc
496
+
497
+ if !ext.nil?
498
+ extension = ext
499
+ elsif pandoc.key? 'to'
500
+ extension = strip_extensions.call(pandoc['to'])
501
+ end
502
+ end
503
+
504
+ DEFAULT_EXTENSION[extension] || extension
505
+ end
756
506
 
757
- # Reset the template with name in this Configuration based on a new
758
- # template
759
- #
760
- # @param name [String] the name of the template in this Configuration
761
- # @param template [Hash] the template to use to update the template in
762
- # this Configuarion with
763
- def reset_template(name, template)
764
- extended_template = extend_template template
765
-
766
- if @templates.has_key? name then
767
- merge @templates[name], extended_template
768
- else
769
- @templates[name] = extended_template
770
- end
771
-
772
- if extended_template.has_key? 'glob' then
773
- @convert_patterns[name] = extended_template['glob']
774
- end
775
- end
507
+ # Is filename a markdown file according to its extension?
508
+ #
509
+ # @param filename [String] the filename to check
510
+ # @return [Boolean] True if filename has a markdown extension.
511
+ def markdown_file?(filename)
512
+ if filename.nil?
513
+ false
514
+ else
515
+ ext = File.extname(filename).delete_prefix('.')
516
+ DEFAULT_EXTENSION.key(ext) == 'markdown'
517
+ end
518
+ end
776
519
 
777
- # Cross-platform way of finding an executable in the $PATH.
778
- #
779
- # which('ruby') #=> /usr/bin/ruby
780
- #
781
- # Taken from:
782
- # http://stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby#5471032
783
- def self.which(cmd)
784
- exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
785
- ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
786
- exts.each { |ext|
787
- exe = File.join(path, "#{cmd}#{ext}")
788
- return exe if File.executable?(exe) and
789
- not File.directory?(exe)
790
- }
791
- end
792
- return nil
793
- end
520
+ # Is there a template with template_name in this Configuration?
521
+ #
522
+ # @param template_name [String] a template's name
523
+ #
524
+ # @return [Boolean] True if there is a template with name equal to
525
+ # template_name in this Configuration
526
+ def template?(template_name)
527
+ @templates.key? template_name
528
+ end
794
529
 
795
- # Rename path by using rename script. If script fails somehow, warn
796
- # and return the original destination.
797
- #
798
- # @param rename_script [String] absolute path to script to run
799
- # @param dst [String] original destination to rename
800
- def rename_destination(rename_script, dst)
801
- script = update_path(rename_script, File.dirname(dst))
530
+ # Get the template with template_name from this Configuration
531
+ #
532
+ # @param template_name [String] a template's name
533
+ #
534
+ # @return [Template] The template with template_name.
535
+ def get_template(template_name)
536
+ @templates[template_name]
537
+ end
802
538
 
803
- command, *parameters = script.shellsplit # split on spaces unless it is preceded by a backslash
539
+ # Determine the template to use with this source document given this
540
+ # Configuration.
541
+ #
542
+ # @param src [String] path to the source document
543
+ # @return [String] the template's name to use
544
+ def determine_template(src)
545
+ @convert_patterns.select do |_, globs|
546
+ globs.any? { |glob| File.fnmatch glob, File.basename(src) }
547
+ end.keys.first
548
+ end
804
549
 
805
- if not File.exists? command
806
- command = Configuration.which(command)
807
- script = "#{command} #{parameters.join(' ')}"
550
+ # Determine the templates to use with this source document given this
551
+ # Configuration.
552
+ #
553
+ # @param src [String] path to the source document
554
+ # @return [Array[String]] the template's name to use
555
+ def determine_templates(src)
556
+ matches = @convert_patterns.select do |_, globs|
557
+ globs.any? { |glob| File.fnmatch glob, File.basename(src) }
558
+ end.keys
559
+
560
+ if matches.empty?
561
+ []
562
+ elsif match_all_templates?
563
+ matches
564
+ else
565
+ [matches.first]
566
+ end
567
+ end
808
568
 
809
- raise ProcessorError.new(:script_does_not_exist, nil, command) if command.nil?
810
- end
569
+ private
570
+
571
+ # Reset the settings for pandocomatic based on a new settings Hash
572
+ #
573
+ # @param settings [Hash] the new settings to use to reset the settings in
574
+ # this Configuration with.
575
+ def reset_settings(settings)
576
+ settings.each do |setting, value|
577
+ case setting
578
+ when 'skip'
579
+ @settings['skip'] = @settings['skip'].concat(value).uniq
580
+ when 'data-dir'
581
+ next # skip data-dir setting; is set once in initialization
582
+ else
583
+ @settings[setting] = value
584
+ end
585
+ end
586
+ end
811
587
 
812
- raise ProcessorError.new(:script_is_not_executable, nil, command) unless File.executable? command
588
+ # Resolve the templates the templates extends and mixes them in, in
589
+ # order of occurrence.
590
+ #
591
+ # @param template [Template] the template to extend
592
+ # @return [Template] the resolved template
593
+ def extend_template(template)
594
+ resolved_template = Template.new template.name
813
595
 
814
- begin
815
- renamed_dst = Processor.run(script, dst)
816
- if not renamed_dst.nil? and not renamed_dst.empty?
817
- renamed_dst.strip
818
- else
819
- raise StandardError,new("Running rename script '#{script}' on destination '#{dst}' did not result in a renamed destination.")
820
- dst
821
- end
822
- rescue StandardError => e
823
- ProcessorError.new(:error_processing_script, e, [script, dst])
824
- dst
825
- end
826
- end
827
-
828
- def marshal_dump()
829
- [@data_dir, @settings, @templates, @convert_patterns]
830
- end
596
+ missing = []
831
597
 
832
- def marshal_load(array)
833
- @data_dir, @settings, @templates, @convert_patterns = array
598
+ template.extends.each do |name|
599
+ if @templates.key? name
600
+ resolved_template.merge! Template.clone(@templates[name])
601
+ else
602
+ missing << name
834
603
  end
604
+ end
835
605
 
836
-
837
- def is_absolute_path?(path)
838
- if Gem.win_platform? then
839
- path.match("^[a-zA-Z]:\\\\\.*$")
840
- else
841
- path.start_with? "/"
842
- end
606
+ unless missing.empty?
607
+ if template.internal?
608
+ warn "WARNING: Unable to find templates [#{missing.join(', ')}] while resolving internal template."
609
+ else
610
+ warn "WARNING: Unable to find templates [#{missing.join(', ')}] while resolving"\
611
+ " the external template '#{template.name}' from configuration file '#{template.path}'."
843
612
  end
613
+ end
844
614
 
845
- def is_root_relative_path?(path)
846
- path.start_with? ROOT_PATH_INDICATOR
847
- end
615
+ resolved_template.merge! template
616
+ resolved_template
617
+ end
848
618
 
849
- def make_path_root_relative(path, dst, root)
850
- # Find how to get to the root directopry from dst directory.
851
- # Assumption is that dst is a subdirectory of root.
852
- dst_dir = File.dirname(File.absolute_path(dst))
853
-
854
- path.delete_prefix! ROOT_PATH_INDICATOR if is_root_relative_path? path
619
+ # Reset the template with name in this Configuration based on a new
620
+ # template
621
+ #
622
+ # @param template [Template] the template to use to update the template in
623
+ # this Configuarion with
624
+ def reset_template(template)
625
+ name = template.name
626
+ extended_template = extend_template template
627
+
628
+ if @templates.key? name
629
+ @templates[name].merge! extended_template
630
+ else
631
+ @templates[name] = extended_template
632
+ end
633
+
634
+ @convert_patterns[name] = extended_template.glob if extended_template.glob?
635
+ end
855
636
 
856
- if File.exist? root and File.realpath("#{dst_dir}").start_with?(File.realpath(root)) then
857
- rel_start = ""
637
+ # Rename path by using rename script. If script fails somehow, warn
638
+ # and return the original destination.
639
+ #
640
+ # @param rename_script [String] absolute path to script to run
641
+ # @param dst [String] original destination to rename
642
+ def rename_destination(rename_script, dst)
643
+ script = Path.update_path(self, rename_script)
644
+
645
+ command, *parameters = script.shellsplit # split on spaces unless it is preceded by a backslash
646
+
647
+ unless File.exist? command
648
+ command = Path.which(command)
649
+ script = "#{command} #{parameters.join(' ')}"
650
+
651
+ raise ProcessorError.new(:script_does_not_exist, nil, command) if command.nil?
652
+ end
653
+
654
+ raise ProcessorError.new(:script_is_not_executable, nil, command) unless File.executable? command
655
+
656
+ begin
657
+ renamed_dst = Processor.run(script, dst)
658
+ if !renamed_dst.nil? && !renamed_dst.empty?
659
+ renamed_dst.strip
660
+ else
661
+ raise StandardError, new("Running rename script '#{script}' on destination '#{dst}'"\
662
+ ' did not result in a renamed destination.')
663
+ end
664
+ rescue StandardError => e
665
+ ProcessorError.new(:error_processing_script, e, [script, dst])
666
+ dst
667
+ end
668
+ end
858
669
 
859
- until File.identical?(File.realpath("#{dst_dir}/#{rel_start}"), File.realpath(root)) do
860
- # invariant dst_dir/rel_start <= root
861
- rel_start += "../"
862
- end
670
+ def marshal_dump
671
+ [@data_dir, @settings, @templates, @convert_patterns]
672
+ end
863
673
 
864
- if rel_start.end_with? "/" and path.start_with? "/" then
865
- "#{rel_start}#{path.delete_prefix("/")}"
866
- else
867
- "#{rel_start}#{path}"
868
- end
869
- else
870
- # Because the destination is not in a subdirectory of root, a
871
- # relative path to that root cannot be created. Instead,
872
- # the path is assumed to be absolute relative to root
873
- root = root.delete_suffix "/" if root.end_with? "/"
874
- path = path.delete_prefix "/" if path.start_with? "/"
674
+ def marshal_load(array)
675
+ @data_dir, @settings, @templates, @convert_patterns = array
676
+ end
875
677
 
876
- "#{root}/#{path}"
877
- end
678
+ def to_stdout?(options)
679
+ !options.nil? and options[:stdout_given] and options[:stdout]
680
+ end
878
681
 
682
+ # Read a list of configuration files and create a
683
+ # pandocomatic object that mixes templates from most generic to most
684
+ # specific.
685
+ def load_configuration_hierarchy(options, data_dirs)
686
+ # Read and mixin templates from most generic config file to most
687
+ # specific, thus in reverse order.
688
+ @config_files = determine_config_files(options, data_dirs).reverse
689
+ @config_files.each do |config_file|
690
+ configure PandocomaticYAML.load_file(config_file), config_file
691
+ rescue StandardError => e
692
+ raise ConfigurationError.new(:unable_to_load_config_file, e, config_file)
693
+ end
694
+
695
+ load @config_files.last
696
+ end
879
697
 
880
- end
698
+ def determine_config_files(options, data_dirs = [])
699
+ config_files = []
700
+ # Get config file from option, if any
701
+ config_files << options[:config] if options[:config_given]
881
702
 
882
- def determine_config_file(options, data_dir = Dir.pwd)
883
- config_file = ''
703
+ # Get config file in each data_dir
704
+ data_dirs.each do |data_dir|
705
+ config_files << File.join(data_dir, CONFIG_FILE) if Dir.entries(data_dir).include? CONFIG_FILE
706
+ end
884
707
 
885
- if options[:config_given]
886
- config_file = options[:config]
887
- elsif Dir.entries(data_dir).include? CONFIG_FILE
888
- config_file = File.join(data_dir, CONFIG_FILE)
889
- elsif Dir.entries(Dir.pwd()).include? CONFIG_FILE
890
- config_file = File.join(Dir.pwd(), CONFIG_FILE)
891
- else
892
- # Fall back to default configuration file distributed with
893
- # pandocomatic
894
- config_file = File.join(__dir__, 'default_configuration.yaml')
895
- end
708
+ # Default configuration file distributes with pandocomatic
709
+ config_files << File.join(__dir__, 'default_configuration.yaml')
896
710
 
897
- path = File.absolute_path config_file
711
+ config_files.map do |config_file|
712
+ path = File.absolute_path config_file
898
713
 
899
- raise ConfigurationError.new(:config_file_does_not_exist, nil, path) unless File.exist? path
900
- raise ConfigurationError.new(:config_file_is_not_a_file, nil, path) unless File.file? path
901
- raise ConfigurationError.new(:config_file_is_not_readable, nil, path) unless File.readable? path
714
+ raise ConfigurationError.new(:config_file_does_not_exist, nil, path) unless File.exist? path
715
+ raise ConfigurationError.new(:config_file_is_not_a_file, nil, path) unless File.file? path
716
+ raise ConfigurationError.new(:config_file_is_not_readable, nil, path) unless File.readable? path
902
717
 
903
- path
904
- end
718
+ path
719
+ end
720
+ end
905
721
 
906
- def determine_data_dir(options)
907
- data_dir = ''
908
-
909
- if options[:data_dir_given]
910
- data_dir = options[:data_dir]
911
- else
912
- # No data-dir option given: try to find the default one from pandoc
913
- begin
914
- data_dir = Paru::Pandoc.info()[:data_dir]
915
-
916
- # If pandoc's data dir does not exist, however, fall back
917
- # to the current directory
918
- unless File.exist? File.absolute_path(data_dir)
919
- data_dir = Dir.pwd
920
- end
921
- rescue Paru::Error => e
922
- # If pandoc cannot be run, continuing probably does not work out
923
- # anyway, so raise pandoc error
924
- raise PandocError.new(:error_running_pandoc, e, data_dir)
925
- rescue StandardError => e
926
- # Ignore error and use the current working directory as default working directory
927
- data_dir = Dir.pwd
928
- end
929
- end
722
+ def determine_config_file(options, data_dir = Dir.pwd)
723
+ determine_config_files(options, [data_dir]).first
724
+ end
930
725
 
931
- # check if data directory does exist and is readable
932
- path = File.absolute_path data_dir
726
+ # Determine all data directories to use
727
+ def determine_data_dirs(options)
728
+ data_dirs = []
933
729
 
934
- raise ConfigurationError.new(:data_dir_does_not_exist, nil, path) unless File.exist? path
935
- raise ConfigurationError.new(:data_dir_is_not_a_directory, nil, path) unless File.directory? path
936
- raise ConfigurationError.new(:data_dir_is_not_readable, nil, path) unless File.readable? path
730
+ # Data dir from CLI option
731
+ data_dirs << options[:data_dir] if options[:data_dir_given]
937
732
 
938
- path
939
- end
733
+ # Pandoc's default data dir
734
+ begin
735
+ data_dir = Paru::Pandoc.info[:data_dir]
940
736
 
941
- def determine_root_path(options)
942
- if options[:root_path_given] then
943
- options[:root_path]
944
- elsif options[:output_given] then
945
- File.absolute_path(File.dirname options[:output])
946
- else
947
- File.absolute_path "."
948
- end
949
- end
737
+ # If pandoc's data dir does not exist, however, fall back
738
+ # to the current directory
739
+ data_dirs << if File.exist? File.absolute_path(data_dir)
740
+ data_dir
741
+ else
742
+ Dir.pwd
743
+ end
744
+ rescue Paru::Error => e
745
+ # If pandoc cannot be run, continuing probably does not work out
746
+ # anyway, so raise pandoc error
747
+ raise PandocError.new(:error_running_pandoc, e, data_dir)
748
+ rescue StandardError
749
+ # Ignore error and use the current working directory as default working directory
750
+ data_dirs << Dir.pwd
751
+ end
752
+
753
+ # check if data directories do exist and are readable
754
+ data_dirs.uniq.map do |dir|
755
+ path = File.absolute_path dir
756
+
757
+ raise ConfigurationError.new(:data_dir_does_not_exist, nil, path) unless File.exist? path
758
+ raise ConfigurationError.new(:data_dir_is_not_a_directory, nil, path) unless File.directory? path
759
+ raise ConfigurationError.new(:data_dir_is_not_readable, nil, path) unless File.readable? path
760
+
761
+ path
762
+ end
950
763
  end
764
+ end
765
+ # rubocop:enable Metrics
951
766
  end