epub_tools 0.3.0 → 0.4.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.document +2 -0
  3. data/.github/workflows/ci.yml +9 -8
  4. data/.gitignore +4 -0
  5. data/.rubocop.yml +41 -0
  6. data/Gemfile +17 -8
  7. data/Gemfile.lock +51 -0
  8. data/LICENSE +21 -0
  9. data/README.md +21 -3
  10. data/bin/epub-tools +3 -109
  11. data/epub_tools.gemspec +6 -8
  12. data/lib/epub_tools/add_chapters.rb +124 -0
  13. data/lib/epub_tools/cli/command_registry.rb +47 -0
  14. data/lib/epub_tools/cli/option_builder.rb +164 -0
  15. data/lib/epub_tools/cli/runner.rb +164 -0
  16. data/lib/epub_tools/cli.rb +45 -0
  17. data/lib/epub_tools/compile_book.rb +77 -34
  18. data/lib/epub_tools/epub_initializer.rb +48 -26
  19. data/lib/epub_tools/loggable.rb +11 -0
  20. data/lib/epub_tools/pack_ebook.rb +20 -13
  21. data/lib/epub_tools/split_chapters.rb +40 -21
  22. data/lib/epub_tools/style_finder.rb +58 -0
  23. data/lib/epub_tools/unpack_ebook.rb +23 -16
  24. data/lib/epub_tools/version.rb +2 -1
  25. data/lib/epub_tools/xhtml_cleaner.rb +28 -8
  26. data/lib/epub_tools/xhtml_extractor.rb +23 -10
  27. data/lib/epub_tools.rb +4 -2
  28. data/test/{add_chapters_to_epub_test.rb → add_chapters_test.rb} +14 -7
  29. data/test/cli/command_registry_test.rb +66 -0
  30. data/test/cli/option_builder_test.rb +173 -0
  31. data/test/cli/runner_test.rb +91 -0
  32. data/test/cli_commands_test.rb +100 -0
  33. data/test/cli_test.rb +4 -0
  34. data/test/cli_version_test.rb +5 -3
  35. data/test/compile_book_test.rb +11 -2
  36. data/test/epub_initializer_test.rb +51 -31
  37. data/test/pack_ebook_test.rb +14 -8
  38. data/test/split_chapters_test.rb +22 -1
  39. data/test/{text_style_class_finder_test.rb → style_finder_test.rb} +7 -6
  40. data/test/test_helper.rb +4 -5
  41. data/test/unpack_ebook_test.rb +21 -5
  42. data/test/xhtml_cleaner_test.rb +13 -7
  43. data/test/xhtml_extractor_test.rb +17 -1
  44. metadata +24 -39
  45. data/lib/epub_tools/add_chapters_to_epub.rb +0 -87
  46. data/lib/epub_tools/cli_helper.rb +0 -31
  47. data/lib/epub_tools/text_style_class_finder.rb +0 -47
@@ -0,0 +1,164 @@
1
+ require 'optparse'
2
+
3
+ module EpubTools
4
+ module CLI
5
+ # Builds and manages command line options
6
+ class OptionBuilder
7
+ attr_reader :options, :required_keys, :parser
8
+
9
+ # Initialize a new OptionBuilder
10
+ # @param default_options [Hash] Default options to start with
11
+ # @param required_keys [Array<Symbol>] Keys that must be present in the final options
12
+ def initialize(default_options = {}, required_keys = [])
13
+ @options = default_options.dup
14
+ @required_keys = required_keys
15
+ @parser = OptionParser.new
16
+ end
17
+
18
+ # Add banner to the option parser
19
+ # @param text [String] Banner text
20
+ # @return [self] for method chaining
21
+ def with_banner(text)
22
+ @parser.banner = text
23
+ self
24
+ end
25
+
26
+ # Add help option to the parser
27
+ # @return [self] for method chaining
28
+ def with_help_option
29
+ @parser.on('-h', '--help', 'Print this help') do
30
+ puts @parser
31
+ exit
32
+ end
33
+ self
34
+ end
35
+
36
+ # Add verbose option to the parser
37
+ # @return [self] for method chaining
38
+ def with_verbose_option
39
+ @options[:verbose] = true unless @options.key?(:verbose)
40
+ @parser.on('-q', '--quiet', 'Run quietly (default: verbose)') { |v| @options[:verbose] = !v }
41
+ self
42
+ end
43
+
44
+ # Add input file option to the parser
45
+ # @param description [String] Option description
46
+ # @param required [Boolean] Whether this option is required
47
+ # @return [self] for method chaining
48
+ def with_input_file(description = 'Input file', required = true)
49
+ desc = required ? "#{description} (required)" : description
50
+ @parser.on('-i FILE', '--input-file FILE', desc) { |v| @options[:input_file] = v }
51
+ self
52
+ end
53
+
54
+ # Add input directory option to the parser
55
+ # @param description [String] Option description
56
+ # @param required [Boolean] Whether this option is required
57
+ # @return [self] for method chaining
58
+ def with_input_dir(description = 'Input directory', required = true)
59
+ desc = required ? "#{description} (required)" : description
60
+ @parser.on('-i DIR', '--input-dir DIR', desc) { |v| @options[:input_dir] = v }
61
+ self
62
+ end
63
+
64
+ # Add output directory option to the parser
65
+ # @param description [String] Option description
66
+ # @param default [String, nil] Default value
67
+ # @return [self] for method chaining
68
+ def with_output_dir(description = 'Output directory', default = nil)
69
+ if default
70
+ desc = "#{description} (default: #{default})"
71
+ @options[:output_dir] = default unless @options.key?(:output_dir)
72
+ else
73
+ desc = "#{description} (required)"
74
+ end
75
+ @parser.on('-o DIR', '--output-dir DIR', desc) { |v| @options[:output_dir] = v }
76
+ self
77
+ end
78
+
79
+ # Add output file option to the parser
80
+ # @param description [String] Option description
81
+ # @param required [Boolean] Whether this option is required
82
+ # @return [self] for method chaining
83
+ def with_output_file(description = 'Output file', required = true)
84
+ desc = required ? "#{description} (required)" : description
85
+ @parser.on('-o FILE', '--output-file FILE', desc) { |v| @options[:output_file] = v }
86
+ self
87
+ end
88
+
89
+ # Add title option to the parser
90
+ # @return [self] for method chaining
91
+ def with_title_option
92
+ @parser.on('-t TITLE', '--title TITLE', 'Book title (required)') { |v| @options[:title] = v }
93
+ self
94
+ end
95
+
96
+ # Add author option to the parser
97
+ # @return [self] for method chaining
98
+ def with_author_option
99
+ @parser.on('-a AUTHOR', '--author AUTHOR', 'Author name (required)') { |v| @options[:author] = v }
100
+ self
101
+ end
102
+
103
+ # Add cover option to the parser
104
+ # @return [self] for method chaining
105
+ def with_cover_option
106
+ @parser.on('-c PATH', '--cover PATH', 'Cover image file path (optional)') { |v| @options[:cover_image] = v }
107
+ self
108
+ end
109
+
110
+ # Add a custom option to the parser
111
+ # @param short [String] Short option flag
112
+ # @param long [String] Long option flag
113
+ # @param description [String] Option description
114
+ # @param option_key [Symbol] Key in the options hash
115
+ # @param block [Proc] Optional block for custom processing
116
+ # @return [self] for method chaining
117
+ def with_option(short, long, description, option_key)
118
+ @parser.on(short, long, description) do |v|
119
+ @options[option_key] = block_given? ? yield(v) : v
120
+ end
121
+ self
122
+ end
123
+
124
+ # Add a custom block to configure options
125
+ # @yield [OptionParser, Hash] Yields the parser and options hash
126
+ # @return [self] for method chaining
127
+ def with_custom_options
128
+ yield @parser, @options if block_given?
129
+ self
130
+ end
131
+
132
+ # Parse the command line arguments
133
+ # @param args [Array<String>] Command line arguments
134
+ # @return [Hash] Parsed options
135
+ # @raise [SystemExit] If required options are missing
136
+ def parse(args = ARGV)
137
+ begin
138
+ @parser.parse!(args.dup)
139
+ validate_required_keys
140
+ rescue ArgumentError => e
141
+ abort "#{e.message}\n#{@parser}"
142
+ end
143
+ @options
144
+ end
145
+
146
+ private
147
+
148
+ # Validate that all required keys are present in the options
149
+ # @raise [SystemExit] If required options are missing
150
+ def validate_required_keys
151
+ return if @required_keys.empty?
152
+
153
+ missing = @required_keys.select { |k| @options[k].nil? }
154
+ return if missing.empty?
155
+
156
+ raise ArgumentError, "Missing required options: #{missing_keys(missing)}"
157
+ end
158
+
159
+ def missing_keys(keys)
160
+ keys.map { |k| "--#{k.to_s.tr('_', '-')}" }.join(', ')
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,164 @@
1
+ require_relative 'command_registry'
2
+ require_relative 'option_builder'
3
+
4
+ module EpubTools
5
+ module CLI
6
+ # Main runner for the CLI application
7
+ class Runner
8
+ attr_reader :registry, :program_name
9
+
10
+ # Initialize a new CLI Runner
11
+ # @param program_name [String] Name of the program
12
+ def initialize(program_name = nil)
13
+ @registry = CommandRegistry.new
14
+ @program_name = program_name || File.basename($PROGRAM_NAME)
15
+ end
16
+
17
+ # Run the CLI application
18
+ # @param args [Array<String>] Command line arguments
19
+ # @return [Boolean] true if the command was run successfully
20
+ def run(args = ARGV)
21
+ # Handle global version flag
22
+ if ['-v', '--version'].include?(args[0])
23
+ puts EpubTools::VERSION
24
+ exit 0
25
+ end
26
+
27
+ commands = registry.available_commands
28
+
29
+ if args.empty? || !commands.include?(args[0])
30
+ print_usage(commands)
31
+ exit 1
32
+ end
33
+
34
+ cmd = args.shift
35
+ handle_command(cmd, args)
36
+ end
37
+
38
+ # Handle a specific command
39
+ # @param cmd [String] Command name
40
+ # @param args [Array<String>] Command line arguments
41
+ # @return [Boolean] true if the command was run successfully
42
+ def handle_command(cmd, args = ARGV)
43
+ command_config = registry.get(cmd)
44
+ return false unless command_config
45
+
46
+ options = command_config[:default_options].dup
47
+ required_keys = command_config[:required_keys]
48
+
49
+ builder = OptionBuilder.new(options, required_keys)
50
+ .with_banner("Usage: #{program_name} #{cmd} [options]")
51
+ .with_help_option
52
+
53
+ # Configure command-specific options
54
+ configure_command_options(cmd, builder)
55
+
56
+ # Parse arguments and run the command
57
+ options = builder.parse(args)
58
+ command_class = command_config[:class]
59
+ command_class.new(options).run
60
+ true
61
+ end
62
+
63
+ private
64
+
65
+ # Print usage information
66
+ # @param commands [Array<String>] Available commands
67
+ def print_usage(_commands)
68
+ puts <<~USAGE
69
+ Usage: #{program_name} COMMAND [options]
70
+ Commands:
71
+ init Initialize a bare-bones EPUB
72
+ extract Extract XHTML files from EPUBs
73
+ split Split XHTML into separate XHTMLs per chapter
74
+ add Add chapter XHTML files into an EPUB
75
+ pack Package an EPUB directory into a .epub file
76
+ unpack Unpack an EPUB file into a directory
77
+ compile Takes EPUBs in a dir and splits, cleans, and compiles into a single EPUB.
78
+ USAGE
79
+ end
80
+
81
+ # Configure command-specific options
82
+ # @param cmd [String] Command name
83
+ # @param builder [OptionBuilder] Option builder instance
84
+ def configure_command_options(cmd, builder)
85
+ case cmd
86
+ when 'add'
87
+ builder.with_custom_options do |opts, options|
88
+ opts.on('-c DIR', '--chapters-dir DIR', 'Chapters directory (required)') { |v| options[:chapters_dir] = v }
89
+ opts.on('-e DIR', '--epub-oebps-dir DIR', 'EPUB OEBPS directory (required)') do |v|
90
+ options[:epub_oebps_dir] = v
91
+ end
92
+ end
93
+
94
+ when 'extract'
95
+ builder.with_custom_options do |opts, options|
96
+ opts.on('-s DIR', '--source-dir DIR', 'Directory with EPUBs to extract XHTMLs from (required)') do |v|
97
+ options[:source_dir] = v
98
+ end
99
+ opts.on('-t DIR', '--target-dir DIR',
100
+ 'Directory where the XHTML files will be extracted to (required)') do |v|
101
+ options[:target_dir] = v
102
+ end
103
+ end
104
+ .with_verbose_option
105
+
106
+ when 'split'
107
+ builder.with_custom_options do |opts, options|
108
+ opts.on('-i FILE', '--input FILE', 'Source XHTML file (required)') { |v| options[:input_file] = v }
109
+ opts.on('-t TITLE', '--title TITLE', 'Book title for HTML <title> tags (required)') do |v|
110
+ options[:book_title] = v
111
+ end
112
+ opts.on('-o DIR', '--output-dir DIR',
113
+ "Output directory for chapter files (default: #{options[:output_dir]})") do |v|
114
+ options[:output_dir] = v
115
+ end
116
+ opts.on('-p PREFIX', '--prefix PREFIX',
117
+ "Filename prefix for chapters (default: #{options[:prefix]})") do |v|
118
+ options[:prefix] = v
119
+ end
120
+ end
121
+ .with_verbose_option
122
+
123
+ when 'init'
124
+ builder.with_title_option
125
+ .with_author_option
126
+ .with_custom_options do |opts, options|
127
+ opts.on('-o DIR', '--output-dir DIR', 'Destination EPUB directory (required)') do |v|
128
+ options[:destination] = v
129
+ end
130
+ end
131
+ .with_cover_option
132
+
133
+ when 'pack'
134
+ builder.with_input_dir('EPUB directory to package')
135
+ .with_output_file('Output EPUB file path')
136
+ .with_verbose_option
137
+
138
+ when 'unpack'
139
+ builder.with_custom_options do |opts, options|
140
+ opts.on('-i FILE', '--input-file FILE', 'EPUB file to unpack (required)') { |v| options[:epub_file] = v }
141
+ opts.on('-o DIR', '--output-dir DIR', 'Output directory to extract into (default: basename of epub)') do |v|
142
+ options[:output_dir] = v
143
+ end
144
+ end
145
+ .with_verbose_option
146
+
147
+ when 'compile'
148
+ builder.with_title_option
149
+ .with_author_option
150
+ .with_custom_options do |opts, options|
151
+ opts.on('-s DIR', '--source-dir DIR', 'Directory with EPUBs to extract XHTMLs from (required)') do |v|
152
+ options[:source_dir] = v
153
+ end
154
+ opts.on('-o FILE', '--output FILE', 'EPUB to create (default: book title in source dir)') do |v|
155
+ options[:output_file] = v
156
+ end
157
+ end
158
+ .with_cover_option
159
+ .with_verbose_option
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,45 @@
1
+ require_relative 'cli/command_registry'
2
+ require_relative 'cli/option_builder'
3
+ require_relative 'cli/runner'
4
+
5
+ module EpubTools
6
+ # CLI module - houses the object-oriented command line interface components
7
+ module CLI
8
+ # Create a new Runner instance configured with all available commands
9
+ # @param program_name [String] Name of the program
10
+ # @return [CLI::Runner] A configured runner instance
11
+ def self.create_runner(program_name = nil)
12
+ runner = Runner.new(program_name)
13
+
14
+ # Register all commands
15
+ runner.registry.register('add', EpubTools::AddChapters,
16
+ %i[chapters_dir epub_oebps_dir])
17
+
18
+ runner.registry.register('extract', EpubTools::XHTMLExtractor,
19
+ %i[source_dir target_dir],
20
+ { verbose: true })
21
+
22
+ runner.registry.register('split', EpubTools::SplitChapters,
23
+ %i[input_file book_title],
24
+ { output_dir: './chapters', prefix: 'chapter', verbose: true })
25
+
26
+ runner.registry.register('init', EpubTools::EpubInitializer,
27
+ %i[title author destination],
28
+ { verbose: true })
29
+
30
+ runner.registry.register('pack', EpubTools::PackEbook,
31
+ %i[input_dir output_file],
32
+ { verbose: true })
33
+
34
+ runner.registry.register('unpack', EpubTools::UnpackEbook,
35
+ [:epub_file],
36
+ { verbose: true })
37
+
38
+ runner.registry.register('compile', EpubTools::CompileBook,
39
+ %i[title author source_dir],
40
+ { verbose: true })
41
+
42
+ runner
43
+ end
44
+ end
45
+ end
@@ -1,27 +1,48 @@
1
1
  #!/usr/bin/env ruby
2
2
  require 'fileutils'
3
+ require_relative 'loggable'
3
4
  require_relative 'xhtml_extractor'
4
5
  require_relative 'split_chapters'
5
6
  require_relative 'epub_initializer'
6
- require_relative 'add_chapters_to_epub'
7
+ require_relative 'add_chapters'
7
8
  require_relative 'pack_ebook'
8
9
 
9
10
  module EpubTools
10
11
  # Orchestrates extraction, splitting, validation, and packaging of book EPUBs
11
12
  class CompileBook
12
- attr_reader :title, :author, :source_dir, :cover_image, :output_file, :build_dir, :verbose
13
-
14
- # title: String, author: String, source_dir: path to input epubs
15
- # cover_image: optional path to cover image, output_file: filename for final epub
16
- # build_dir: optional working directory for intermediate files
17
- def initialize(title:, author:, source_dir:, cover_image: nil, output_file: nil, build_dir: nil, verbose: false)
18
- @title = title
19
- @author = author
20
- @source_dir = source_dir
21
- @cover_image = cover_image
22
- @output_file = output_file || default_output_file
23
- @build_dir = build_dir || File.join(Dir.pwd, '.epub_tools_build')
24
- @verbose = verbose
13
+ include Loggable
14
+ # Book title
15
+ attr_reader :title
16
+ # Book author
17
+ attr_reader :author
18
+ # Path of the input epubs
19
+ attr_reader :source_dir
20
+ # Optional path to the cover image
21
+ attr_reader :cover_image
22
+ # Filename for the final epub
23
+ attr_reader :output_file
24
+ # Optional working directory for intermediate files
25
+ attr_reader :build_dir
26
+ # Whether to print progress to STDOUT
27
+ attr_reader :verbose
28
+
29
+ # Initializes the class
30
+ # @param options [Hash] Configuration options
31
+ # @option options [String] :title Book title (required)
32
+ # @option options [String] :author Book author (required)
33
+ # @option options [String] :source_dir Path of the input epubs (required)
34
+ # @option options [String] :cover_image Optional path to the cover image
35
+ # @option options [String] :output_file Filename for the final epub (default: [title].epub)
36
+ # @option options [String] :build_dir Optional working directory for intermediate files
37
+ # @option options [Boolean] :verbose Whether to print progress to STDOUT (default: false)
38
+ def initialize(options = {})
39
+ @title = options.fetch(:title)
40
+ @author = options.fetch(:author)
41
+ @source_dir = options.fetch(:source_dir)
42
+ @cover_image = options[:cover_image]
43
+ @output_file = options[:output_file] || default_output_file
44
+ @build_dir = options[:build_dir] || File.join(Dir.pwd, '.epub_tools_build')
45
+ @verbose = options[:verbose] || false
25
46
  end
26
47
 
27
48
  # Run the full compile workflow
@@ -40,10 +61,6 @@ module EpubTools
40
61
 
41
62
  private
42
63
 
43
- def log(message)
44
- puts message if verbose
45
- end
46
-
47
64
  def default_output_file
48
65
  "#{title.gsub(' ', '_')}.epub"
49
66
  end
@@ -54,7 +71,7 @@ module EpubTools
54
71
  end
55
72
 
56
73
  def prepare_dirs
57
- log "Preparing build directories..."
74
+ log 'Preparing build directories...'
58
75
  FileUtils.mkdir_p(xhtml_dir)
59
76
  FileUtils.mkdir_p(chapters_dir)
60
77
  end
@@ -73,52 +90,78 @@ module EpubTools
73
90
 
74
91
  def extract_xhtmls
75
92
  log "Extracting XHTML files from epubs in '#{source_dir}'..."
76
- XHTMLExtractor.new(source_dir: source_dir, target_dir: xhtml_dir, verbose: verbose).extract_all
93
+ XHTMLExtractor.new({
94
+ source_dir: source_dir,
95
+ target_dir: xhtml_dir,
96
+ verbose: verbose
97
+ }).run
77
98
  end
78
99
 
79
100
  def split_xhtmls
80
- log "Splitting XHTML files into chapters..."
101
+ log 'Splitting XHTML files into chapters...'
81
102
  Dir.glob(File.join(xhtml_dir, '*.xhtml')).each do |xhtml_file|
82
103
  base = File.basename(xhtml_file, '.xhtml')
83
104
  log "Splitting '#{base}'..."
84
- SplitChapters.new(xhtml_file, title, chapters_dir, 'chapter', verbose).run
105
+ SplitChapters.new({
106
+ input_file: xhtml_file,
107
+ book_title: title,
108
+ output_dir: chapters_dir,
109
+ output_prefix: 'chapter',
110
+ verbose: verbose
111
+ }).run
85
112
  end
86
113
  end
87
114
 
88
115
  def validate_sequence
89
- log "Validating chapter sequence..."
116
+ log 'Validating chapter sequence...'
90
117
  nums = Dir.glob(File.join(chapters_dir, '*.xhtml')).map do |file|
91
118
  if (m = File.basename(file, '.xhtml').match(/_(\d+)\z/))
92
119
  m[1].to_i
93
120
  end
94
121
  end.compact
95
122
  raise "No chapter files found in #{chapters_dir}" if nums.empty?
123
+
96
124
  sorted = nums.sort.uniq
97
125
  missing = (sorted.first..sorted.last).to_a - sorted
98
- if missing.any?
99
- raise "Missing chapter numbers: #{missing.join(' ')}"
100
- else
101
- log "Chapter sequence is complete: #{sorted.first} to #{sorted.last}."
102
- end
126
+ raise "Missing chapter numbers: #{missing.join(' ')}" if missing.any?
127
+
128
+ log "Chapter sequence is complete: #{sorted.first} to #{sorted.last}."
103
129
  end
104
130
 
105
131
  def initialize_epub
106
- log "Initializing new EPUB..."
132
+ log 'Initializing new EPUB...'
107
133
  if cover_image
108
- EpubInitializer.new(title, author, epub_dir, cover_image).run
134
+ EpubInitializer.new({
135
+ title: title,
136
+ author: author,
137
+ destination: epub_dir,
138
+ cover_image: cover_image
139
+ }).run
109
140
  else
110
- EpubInitializer.new(title, author, epub_dir).run
141
+ EpubInitializer.new({
142
+ title: title,
143
+ author: author,
144
+ destination: epub_dir
145
+ }).run
111
146
  end
112
147
  end
113
148
 
114
149
  def add_chapters
115
- log "Adding chapters to EPUB..."
116
- AddChaptersToEpub.new(chapters_dir, File.join(epub_dir, 'OEBPS'), verbose).run
150
+ log 'Adding chapters to EPUB...'
151
+ AddChapters.new({
152
+ chapters_dir: chapters_dir,
153
+ epub_dir: File.join(epub_dir, 'OEBPS'),
154
+ verbose: verbose
155
+ }).run
117
156
  end
118
157
 
119
158
  def pack_epub
120
159
  log "Building final EPUB '#{output_file}'..."
121
- PackEbook.new(epub_dir, output_file, verbose: verbose).run
160
+ PackEbook.new({
161
+ input_dir: epub_dir,
162
+ output_file: output_file,
163
+ verbose: verbose
164
+ }).run
122
165
  end
123
166
  end
124
167
  end
@@ -2,22 +2,39 @@
2
2
  require 'fileutils'
3
3
  require 'time'
4
4
  require 'securerandom'
5
+ require_relative 'loggable'
5
6
 
6
7
  module EpubTools
8
+ # Sets up a basic empty EPUB directory structure with the basic files created:
9
+ # - +mimetype+
10
+ # - +container.xml+
11
+ # - +title.xhtml+ as a title page
12
+ # - +package.opf+
13
+ # - +nav.xhtml+ as a table of contents
14
+ # - +style.css+ a basic style inherited from the repo
15
+ # - cover image (optionally)
7
16
  class EpubInitializer
8
- # title: book title; author: author name; destination: output EPUB directory
9
- # cover_image: optional path to cover image file
10
- def initialize(title, author, destination, cover_image = nil)
11
- @title = title
12
- @author = author
13
- @destination = File.expand_path(destination)
17
+ include Loggable
18
+ # Initializes the class
19
+ # @param options [Hash] Configuration options
20
+ # @option options [String] :title Book title (required)
21
+ # @option options [String] :author Book author (required)
22
+ # @option options [String] :destination Target directory for the EPUB files (required)
23
+ # @option options [String] :cover_image Optional path to the cover image
24
+ # @option options [Boolean] :verbose Whether to print progress to STDOUT (default: false)
25
+ def initialize(options = {})
26
+ @title = options.fetch(:title)
27
+ @author = options.fetch(:author)
28
+ @destination = File.expand_path(options.fetch(:destination))
14
29
  @uuid = "urn:uuid:#{SecureRandom.uuid}"
15
30
  @modified = Time.now.utc.iso8601
16
- @cover_image_path = cover_image
31
+ @cover_image_path = options[:cover_image]
17
32
  @cover_image_fname = nil
18
33
  @cover_image_media_type = nil
34
+ @verbose = options[:verbose] || false
19
35
  end
20
36
 
37
+ # Creates the empty ebook and returns the directory
21
38
  def run
22
39
  create_structure
23
40
  write_mimetype
@@ -27,6 +44,8 @@ module EpubTools
27
44
  write_package_opf
28
45
  write_nav
29
46
  write_style
47
+ log "Created empty ebook structure at: #{@destination}"
48
+ @destination
30
49
  end
31
50
 
32
51
  private
@@ -37,7 +56,7 @@ module EpubTools
37
56
  end
38
57
 
39
58
  def write_mimetype
40
- File.write("#{@destination}/mimetype", "application/epub+zip")
59
+ File.write("#{@destination}/mimetype", 'application/epub+zip')
41
60
  end
42
61
 
43
62
  def write_title_page
@@ -114,36 +133,39 @@ module EpubTools
114
133
  File.write(File.join(@destination, 'OEBPS', 'cover.xhtml'), content)
115
134
  end
116
135
 
136
+ def mitem(id, href, type, properties = nil)
137
+ xml = "<item id=\"#{id}\" href=\"#{href}\" media-type=\"#{type}\""
138
+ xml += " properties=\"#{properties}\"" if properties
139
+ "#{xml}/>"
140
+ end
141
+
117
142
  # Generates the package.opf with optional cover image entries
118
143
  def write_package_opf
119
144
  manifest_items = []
120
145
  spine_items = []
121
-
122
- manifest_items << '<item id="style" href="style.css" media-type="text/css"/>'
123
- manifest_items << '<item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/>'
146
+ manifest_items << mitem('style', 'style.css', 'text/css')
147
+ manifest_items << mitem('nav', 'nav.xhtml', 'application/xhtml+xml', 'nav')
124
148
 
125
149
  if @cover_image_fname
126
- manifest_items << %Q{<item id="cover-image" href="#{@cover_image_fname}" media-type="#{@cover_image_media_type}" properties="cover-image"/>}
127
- manifest_items << '<item id="cover-page" href="cover.xhtml" media-type="application/xhtml+xml"/>'
150
+ manifest_items << mitem('cover-image', @cover_image_fname, @cover_image_media_type, 'cover-image')
151
+ manifest_items << mitem('cover-page', 'cover.xhtml', 'application/xhtml+xml')
128
152
  spine_items << '<itemref idref="cover-page"/>'
129
153
  end
130
154
 
131
- manifest_items << '<item id="title" href="title.xhtml" media-type="application/xhtml+xml"/>'
155
+ manifest_items << mitem('title', 'title.xhtml', 'application/xhtml+xml')
132
156
  spine_items << '<itemref idref="title"/>'
133
157
 
134
158
  metadata = []
135
- metadata << %Q{<dc:identifier id="pub-id">#{@uuid}</dc:identifier>}
136
- metadata << %Q{<dc:title>#{@title}</dc:title>}
137
- metadata << %Q{<dc:creator>#{@author}</dc:creator>}
138
- metadata << "<dc:language>en</dc:language>"
139
- metadata << %Q{<meta property="dcterms:modified">#{@modified}</meta>}
140
- metadata << %Q{<meta property="schema:accessMode">textual</meta>}
141
- metadata << %Q{<meta property="schema:accessibilityFeature">unknown</meta>}
142
- metadata << %Q{<meta property="schema:accessibilityHazard">none</meta>}
143
- metadata << %Q{<meta property="schema:accessModeSufficient">textual</meta>}
144
- if @cover_image_fname
145
- metadata << %Q{<meta name="cover" content="cover-image"/>}
146
- end
159
+ metadata << %(<dc:identifier id="pub-id">#{@uuid}</dc:identifier>)
160
+ metadata << %(<dc:title>#{@title}</dc:title>)
161
+ metadata << %(<dc:creator>#{@author}</dc:creator>)
162
+ metadata << '<dc:language>en</dc:language>'
163
+ metadata << %(<meta property="dcterms:modified">#{@modified}</meta>)
164
+ metadata << %(<meta property="schema:accessMode">textual</meta>)
165
+ metadata << %(<meta property="schema:accessibilityFeature">unknown</meta>)
166
+ metadata << %(<meta property="schema:accessibilityHazard">none</meta>)
167
+ metadata << %(<meta property="schema:accessModeSufficient">textual</meta>)
168
+ metadata << %(<meta name="cover" content="cover-image"/>) if @cover_image_fname
147
169
 
148
170
  content = <<~XML
149
171
  <?xml version="1.0" encoding="utf-8"?>