pandocomatic 0.2.8 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 -290
  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 +69 -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