epub_tools 0.4.1 → 0.6.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +3 -0
  3. data/.rubocop.yml +10 -17
  4. data/CLAUDE.md +128 -0
  5. data/Gemfile +4 -4
  6. data/Gemfile.lock +39 -34
  7. data/README.md +37 -24
  8. data/Rakefile +2 -0
  9. data/bin/epub-tools +2 -0
  10. data/epub_tools.gemspec +3 -1
  11. data/lib/epub_tools/add_chapters.rb +64 -33
  12. data/lib/epub_tools/append_book.rb +81 -0
  13. data/lib/epub_tools/book_builder.rb +108 -0
  14. data/lib/epub_tools/chapter_marker_detector.rb +46 -0
  15. data/lib/epub_tools/chapter_validator.rb +50 -0
  16. data/lib/epub_tools/cli/command_options_configurator.rb +128 -0
  17. data/lib/epub_tools/cli/command_registry.rb +2 -0
  18. data/lib/epub_tools/cli/option_builder.rb +5 -3
  19. data/lib/epub_tools/cli/runner.rb +60 -110
  20. data/lib/epub_tools/cli.rb +17 -29
  21. data/lib/epub_tools/compile_book.rb +15 -146
  22. data/lib/epub_tools/compile_workspace.rb +40 -0
  23. data/lib/epub_tools/epub_configuration.rb +33 -0
  24. data/lib/epub_tools/epub_file_writer.rb +57 -0
  25. data/lib/epub_tools/epub_initializer.rb +83 -162
  26. data/lib/epub_tools/epub_metadata_builder.rb +92 -0
  27. data/lib/epub_tools/loggable.rb +2 -0
  28. data/lib/epub_tools/pack_ebook.rb +28 -14
  29. data/lib/epub_tools/split_chapters.rb +44 -56
  30. data/lib/epub_tools/style_finder.rb +17 -6
  31. data/lib/epub_tools/unpack_ebook.rb +20 -10
  32. data/lib/epub_tools/version.rb +3 -1
  33. data/lib/epub_tools/xhtml_cleaner.rb +1 -0
  34. data/lib/epub_tools/xhtml_extractor.rb +20 -10
  35. data/lib/epub_tools/xhtml_generator.rb +71 -0
  36. data/lib/epub_tools.rb +5 -0
  37. data/test/add_chapters_test.rb +119 -25
  38. data/test/append_book_test.rb +127 -0
  39. data/test/chapter_validator_test.rb +74 -0
  40. data/test/cli/command_registry_test.rb +2 -0
  41. data/test/cli/option_builder_test.rb +24 -14
  42. data/test/cli/runner_test.rb +15 -15
  43. data/test/cli_commands_test.rb +11 -0
  44. data/test/cli_test.rb +2 -0
  45. data/test/cli_version_test.rb +2 -0
  46. data/test/compile_book_test.rb +16 -102
  47. data/test/compile_workspace_test.rb +55 -0
  48. data/test/epub_initializer_test.rb +55 -27
  49. data/test/pack_ebook_test.rb +33 -9
  50. data/test/split_chapters_test.rb +96 -7
  51. data/test/style_finder_test.rb +2 -0
  52. data/test/test_helper.rb +2 -0
  53. data/test/unpack_ebook_test.rb +45 -20
  54. data/test/xhtml_cleaner_test.rb +2 -0
  55. data/test/xhtml_extractor_test.rb +3 -1
  56. metadata +17 -3
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+ require_relative 'book_builder'
5
+ require_relative 'unpack_ebook'
6
+
7
+ module EpubTools
8
+ # Appends chapters from source EPUBs to an existing target EPUB
9
+ class AppendBook < BookBuilder
10
+ attr_reader :target_epub
11
+
12
+ def initialize(options = {})
13
+ super
14
+ @target_epub = File.expand_path(options.fetch(:target_epub))
15
+ end
16
+
17
+ private
18
+
19
+ def book_title
20
+ @book_title ||= read_target_title
21
+ end
22
+
23
+ def output_path = @target_epub
24
+
25
+ def prepare_epub
26
+ backup_target
27
+ unpack_target
28
+ end
29
+
30
+ def before_add_chapters
31
+ detect_conflicts
32
+ end
33
+
34
+ def finalize_and_cleanup
35
+ log "Done. Updated EPUB: #{@target_epub} (backup: #{@backup_path})"
36
+ @workspace.clean
37
+ @target_epub
38
+ end
39
+
40
+ def backup_target
41
+ @backup_path = "#{@target_epub}.bak"
42
+ log "Backing up target to '#{@backup_path}'..."
43
+ FileUtils.cp(@target_epub, @backup_path)
44
+ end
45
+
46
+ def unpack_target
47
+ log 'Unpacking target EPUB...'
48
+ UnpackEbook.new(epub_file: @target_epub, output_dir: @workspace.epub_dir, verbose: verbose).run
49
+ end
50
+
51
+ def read_target_title
52
+ opf_path = File.join(epub_oebps_dir, 'package.opf')
53
+ doc = Nokogiri::XML(File.read(opf_path))
54
+ doc.remove_namespaces!
55
+ doc.at_xpath('//title')&.text || 'Untitled'
56
+ end
57
+
58
+ def detect_conflicts
59
+ new_numbers = chapter_numbers_in(@workspace.chapters_dir)
60
+ existing_numbers = chapter_numbers_in(epub_oebps_dir)
61
+ conflicts = new_numbers & existing_numbers
62
+ return if conflicts.empty?
63
+
64
+ formatted = conflicts.sort.map { |n| n == n.to_i ? n.to_i.to_s : n.to_s }
65
+ raise ArgumentError,
66
+ "Chapter number conflict: chapters #{formatted.join(', ')} already exist in the target EPUB. " \
67
+ 'Renumber the source chapters or remove conflicting chapters from the target.'
68
+ end
69
+
70
+ def chapter_numbers_in(dir)
71
+ Dir.glob(File.join(dir, 'chapter_*.xhtml')).filter_map do |path|
72
+ basename = File.basename(path, '.xhtml')
73
+ if (m = basename.match(/_(\d+)_5\z/))
74
+ m[1].to_f + 0.5
75
+ elsif (m = basename.match(/_(\d+)\z/))
76
+ m[1].to_f
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require_relative 'loggable'
5
+ require_relative 'xhtml_extractor'
6
+ require_relative 'split_chapters'
7
+ require_relative 'add_chapters'
8
+ require_relative 'pack_ebook'
9
+ require_relative 'compile_workspace'
10
+ require_relative 'chapter_validator'
11
+
12
+ module EpubTools
13
+ # Base class for book-building workflows (compile and append).
14
+ # Uses template method pattern — subclasses override hooks to customize behavior.
15
+ class BookBuilder
16
+ include Loggable
17
+
18
+ attr_reader :source_dir, :build_dir, :verbose
19
+
20
+ def initialize(options = {})
21
+ @source_dir = options.fetch(:source_dir)
22
+ @build_dir = options[:build_dir] || File.join(Dir.pwd, '.epub_tools_build')
23
+ @verbose = options[:verbose] || false
24
+ @workspace = CompileWorkspace.new(@build_dir)
25
+ end
26
+
27
+ # Run the full build workflow
28
+ # @return [String] Path to the output EPUB file
29
+ def run
30
+ setup_workspace
31
+ prepare_epub
32
+ extract_xhtmls
33
+ split_xhtmls
34
+ validate_chapters
35
+ before_add_chapters
36
+ add_chapters
37
+ pack_epub
38
+ finalize_and_cleanup
39
+ end
40
+
41
+ private
42
+
43
+ # Hook: called before extract/split to set up the EPUB target
44
+ def prepare_epub; end
45
+
46
+ # Hook: called after validation, before adding chapters
47
+ def before_add_chapters; end
48
+
49
+ # Subclasses must implement: the book title used when splitting chapters
50
+ def book_title
51
+ raise NotImplementedError, "#{self.class} must implement #book_title"
52
+ end
53
+
54
+ # Subclasses must implement: the output file path for pack_epub
55
+ def output_path
56
+ raise NotImplementedError, "#{self.class} must implement #output_path"
57
+ end
58
+
59
+ def setup_workspace
60
+ @workspace.clean
61
+ @workspace.prepare_directories
62
+ log 'Preparing build directories...'
63
+ end
64
+
65
+ def extract_xhtmls
66
+ log "Extracting XHTML files from EPUBs in '#{source_dir}'..."
67
+ XHTMLExtractor.new(source_dir: source_dir, target_dir: @workspace.xhtml_dir, verbose: verbose).run
68
+ end
69
+
70
+ def split_xhtmls
71
+ Dir.glob(File.join(@workspace.xhtml_dir, '*.xhtml')).each { |f| split_xhtml_file(f) }
72
+ end
73
+
74
+ def split_xhtml_file(xhtml_file)
75
+ log "Splitting '#{File.basename(xhtml_file, '.xhtml')}'..."
76
+ SplitChapters.new(
77
+ input_file: xhtml_file, book_title: book_title,
78
+ output_dir: @workspace.chapters_dir, output_prefix: 'chapter', verbose: verbose
79
+ ).run
80
+ end
81
+
82
+ def validate_chapters
83
+ ChapterValidator.new(chapters_dir: @workspace.chapters_dir, verbose: verbose).validate
84
+ end
85
+
86
+ def add_chapters
87
+ log 'Adding chapters to EPUB...'
88
+ AddChapters.new(
89
+ chapters_dir: @workspace.chapters_dir,
90
+ oebps_dir: epub_oebps_dir,
91
+ verbose: verbose
92
+ ).run
93
+ end
94
+
95
+ def pack_epub
96
+ log "Building EPUB '#{output_path}'..."
97
+ PackEbook.new(input_dir: @workspace.epub_dir, output_file: output_path, verbose: verbose).run
98
+ end
99
+
100
+ def finalize_and_cleanup
101
+ log "Done. Output EPUB: #{File.expand_path(output_path)}"
102
+ @workspace.clean
103
+ output_path
104
+ end
105
+
106
+ def epub_oebps_dir = File.join(@workspace.epub_dir, 'OEBPS')
107
+ end
108
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EpubTools
4
+ # Detects chapter boundary markers in XHTML nodes.
5
+ # Recognizes: "Chapter N", "Chapter N (continued)", and "Prologue".
6
+ class ChapterMarkerDetector
7
+ # Tags that can contain chapter markers
8
+ MARKER_TAGS = %w[p span h2 h3 h4].freeze
9
+ # Tags that can contain prologue markers
10
+ PROLOGUE_TAGS = %w[h3 h4].freeze
11
+
12
+ # Detect what type of chapter marker a node represents
13
+ # @param node [Nokogiri::XML::Node] The XHTML node to check
14
+ # @return [Symbol, nil] :chapter, :continued, :prologue, or nil
15
+ def detect(node)
16
+ if continued_marker?(node)
17
+ :continued
18
+ elsif chapter_marker?(node)
19
+ :chapter
20
+ elsif prologue_marker?(node)
21
+ :prologue
22
+ end
23
+ end
24
+
25
+ # Extract the chapter number from a node's text
26
+ # @param node [Nokogiri::XML::Node] A node containing "Chapter N" text
27
+ # @return [Integer] The chapter number
28
+ def extract_chapter_number(node)
29
+ node.text.match(/Chapter\s+(\d+)/i)[1].to_i
30
+ end
31
+
32
+ private
33
+
34
+ def continued_marker?(node)
35
+ MARKER_TAGS.include?(node.name) && node.text.match?(/Chapter\s+\d+\s*\(continued\)/i)
36
+ end
37
+
38
+ def chapter_marker?(node)
39
+ MARKER_TAGS.include?(node.name) && node.text.match?(/Chapter\s+\d+/i) && !continued_marker?(node)
40
+ end
41
+
42
+ def prologue_marker?(node)
43
+ PROLOGUE_TAGS.include?(node.name) && node.text.strip.match?(/\APrologue\z/i)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'loggable'
4
+
5
+ module EpubTools
6
+ # Validates chapter sequence completeness
7
+ class ChapterValidator
8
+ include Loggable
9
+
10
+ def initialize(chapters_dir:, verbose: false)
11
+ @chapters_dir = chapters_dir
12
+ @verbose = verbose
13
+ end
14
+
15
+ # Validates that integer chapter numbers form a complete sequence with no gaps.
16
+ # Half-chapters (e.g. chapter_5_5.xhtml) are recognized but not required.
17
+ # @raise [RuntimeError] if no chapter files are found or if integer chapters have gaps
18
+ def validate
19
+ log 'Validating chapter sequence...'
20
+ nums = extract_chapter_numbers
21
+ check_sequence_completeness(nums)
22
+ log "Chapter sequence is complete: #{nums.first} to #{nums.last}."
23
+ end
24
+
25
+ private
26
+
27
+ def extract_chapter_numbers
28
+ nums = Dir.glob(File.join(@chapters_dir, '*.xhtml')).filter_map do |file|
29
+ extract_chapter_number(File.basename(file, '.xhtml'))
30
+ end
31
+ raise "No chapter files found in #{@chapters_dir}" if nums.empty?
32
+
33
+ nums.sort.uniq
34
+ end
35
+
36
+ def extract_chapter_number(basename)
37
+ if (m = basename.match(/_(\d+)_5\z/))
38
+ m[1].to_i + 0.5
39
+ elsif (m = basename.match(/_(\d+)\z/))
40
+ m[1].to_i
41
+ end
42
+ end
43
+
44
+ def check_sequence_completeness(sorted)
45
+ integers = sorted.select { |n| n == n.to_i }.map(&:to_i)
46
+ missing = (integers.first..integers.last).to_a - integers
47
+ raise "Missing chapter numbers: #{missing.join(' ')}" if missing.any?
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EpubTools
4
+ module CLI
5
+ # Handles command-specific option configuration for CLI commands
6
+ class CommandOptionsConfigurator
7
+ # Configure command-specific options using dynamic dispatch
8
+ # @param cmd [String] Command name
9
+ # @param builder [OptionBuilder] Option builder instance
10
+ def configure(cmd, builder)
11
+ method_name = "configure_#{cmd.tr('-', '_')}_options"
12
+ raise ArgumentError, "Unknown command: #{cmd}" unless respond_to?(method_name, true)
13
+
14
+ send(method_name, builder)
15
+ end
16
+
17
+ private
18
+
19
+ # Configure options for the 'add' command
20
+ # @param builder [OptionBuilder] Option builder instance
21
+ def configure_add_options(builder)
22
+ builder.with_custom_options do |opts, options|
23
+ opts.on('-c DIR', '--chapters-dir DIR', 'Chapters directory (required)') { |v| options[:chapters_dir] = v }
24
+ opts.on('-e DIR', '--oebps-dir DIR', 'EPUB OEBPS directory (required)') { |v| options[:oebps_dir] = v }
25
+ end
26
+ end
27
+
28
+ # Configure options for the 'extract' command
29
+ # @param builder [OptionBuilder] Option builder instance
30
+ def configure_extract_options(builder)
31
+ builder.with_custom_options do |opts, options|
32
+ opts.on('-s DIR', '--source-dir DIR', 'Directory with EPUBs to extract XHTMLs from (required)') do |v|
33
+ options[:source_dir] = v
34
+ end
35
+ opts.on('-t DIR', '--target-dir DIR',
36
+ 'Directory where the XHTML files will be extracted to (required)') do |v|
37
+ options[:target_dir] = v
38
+ end
39
+ end.with_verbose_option
40
+ end
41
+
42
+ # Configure options for the 'split' command
43
+ # @param builder [OptionBuilder] Option builder instance
44
+ def configure_split_options(builder)
45
+ builder.with_custom_options do |opts, options|
46
+ add_split_input_options(opts, options)
47
+ add_split_output_options(opts, options)
48
+ end.with_verbose_option
49
+ end
50
+
51
+ def add_split_input_options(opts, options)
52
+ opts.on('-i FILE', '--input FILE', 'Source XHTML file (required)') { |v| options[:input_file] = v }
53
+ opts.on('-t TITLE', '--title TITLE', 'Book title for HTML <title> tags (required)') do |v|
54
+ options[:book_title] = v
55
+ end
56
+ end
57
+
58
+ def add_split_output_options(opts, options)
59
+ opts.on('-o DIR', '--output-dir DIR',
60
+ "Output directory for chapter files (default: #{options[:output_dir]})") do |v|
61
+ options[:output_dir] = v
62
+ end
63
+ opts.on('-p PREFIX', '--prefix PREFIX', "Filename prefix for chapters (default: #{options[:prefix]})") do |v|
64
+ options[:prefix] = v
65
+ end
66
+ end
67
+
68
+ # Configure options for the 'init' command
69
+ # @param builder [OptionBuilder] Option builder instance
70
+ def configure_init_options(builder)
71
+ builder.with_title_option
72
+ .with_author_option
73
+ .with_custom_options do |opts, options|
74
+ opts.on('-o DIR', '--output-dir DIR', 'Destination EPUB directory (required)') do |v|
75
+ options[:destination] = v
76
+ end
77
+ end.with_cover_option
78
+ end
79
+
80
+ # Configure options for the 'pack' command
81
+ # @param builder [OptionBuilder] Option builder instance
82
+ def configure_pack_options(builder)
83
+ builder.with_input_dir('EPUB directory to package')
84
+ .with_output_file('Output EPUB file path')
85
+ .with_verbose_option
86
+ end
87
+
88
+ # Configure options for the 'unpack' command
89
+ # @param builder [OptionBuilder] Option builder instance
90
+ def configure_unpack_options(builder)
91
+ builder.with_custom_options do |opts, options|
92
+ opts.on('-i FILE', '--input-file FILE', 'EPUB file to unpack (required)') { |v| options[:epub_file] = v }
93
+ opts.on('-o DIR', '--output-dir DIR', 'Output directory to extract into (default: basename of epub)') do |v|
94
+ options[:output_dir] = v
95
+ end
96
+ end.with_verbose_option
97
+ end
98
+
99
+ # Configure options for the 'append' command
100
+ # @param builder [OptionBuilder] Option builder instance
101
+ def configure_append_options(builder)
102
+ builder.with_custom_options do |opts, options|
103
+ opts.on('-s DIR', '--source-dir DIR', 'Directory with EPUBs to append (required)') do |v|
104
+ options[:source_dir] = v
105
+ end
106
+ opts.on('-t FILE', '--target-epub FILE', 'Existing EPUB file to append to (required)') do |v|
107
+ options[:target_epub] = v
108
+ end
109
+ end.with_verbose_option
110
+ end
111
+
112
+ # Configure options for the 'compile' command
113
+ # @param builder [OptionBuilder] Option builder instance
114
+ def configure_compile_options(builder)
115
+ builder.with_title_option
116
+ .with_author_option
117
+ .with_custom_options do |opts, options|
118
+ opts.on('-s DIR', '--source-dir DIR', 'Directory with EPUBs to extract XHTMLs from (required)') do |v|
119
+ options[:source_dir] = v
120
+ end
121
+ opts.on('-o FILE', '--output FILE', 'EPUB to create (default: book title in source dir)') do |v|
122
+ options[:output_file] = v
123
+ end
124
+ end.with_cover_option.with_verbose_option
125
+ end
126
+ end
127
+ end
128
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module EpubTools
2
4
  module CLI
3
5
  # Manages the registration and retrieval of commands
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'optparse'
2
4
 
3
5
  module EpubTools
@@ -45,7 +47,7 @@ module EpubTools
45
47
  # @param description [String] Option description
46
48
  # @param required [Boolean] Whether this option is required
47
49
  # @return [self] for method chaining
48
- def with_input_file(description = 'Input file', required = true)
50
+ def with_input_file(description = 'Input file', required: true)
49
51
  desc = required ? "#{description} (required)" : description
50
52
  @parser.on('-i FILE', '--input-file FILE', desc) { |v| @options[:input_file] = v }
51
53
  self
@@ -55,7 +57,7 @@ module EpubTools
55
57
  # @param description [String] Option description
56
58
  # @param required [Boolean] Whether this option is required
57
59
  # @return [self] for method chaining
58
- def with_input_dir(description = 'Input directory', required = true)
60
+ def with_input_dir(description = 'Input directory', required: true)
59
61
  desc = required ? "#{description} (required)" : description
60
62
  @parser.on('-i DIR', '--input-dir DIR', desc) { |v| @options[:input_dir] = v }
61
63
  self
@@ -80,7 +82,7 @@ module EpubTools
80
82
  # @param description [String] Option description
81
83
  # @param required [Boolean] Whether this option is required
82
84
  # @return [self] for method chaining
83
- def with_output_file(description = 'Output file', required = true)
85
+ def with_output_file(description = 'Output file', required: true)
84
86
  desc = required ? "#{description} (required)" : description
85
87
  @parser.on('-o FILE', '--output-file FILE', desc) { |v| @options[:output_file] = v }
86
88
  self
@@ -1,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'command_registry'
2
4
  require_relative 'option_builder'
5
+ require_relative 'command_options_configurator'
3
6
 
4
7
  module EpubTools
5
8
  module CLI
@@ -12,24 +15,15 @@ module EpubTools
12
15
  def initialize(program_name = nil)
13
16
  @registry = CommandRegistry.new
14
17
  @program_name = program_name || File.basename($PROGRAM_NAME)
18
+ @options_configurator = CommandOptionsConfigurator.new
15
19
  end
16
20
 
17
21
  # Run the CLI application
18
22
  # @param args [Array<String>] Command line arguments
19
23
  # @return [Boolean] true if the command was run successfully
20
24
  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
25
+ handle_version_flag(args)
26
+ validate_command_args(args)
33
27
 
34
28
  cmd = args.shift
35
29
  handle_command(cmd, args)
@@ -43,6 +37,36 @@ module EpubTools
43
37
  command_config = registry.get(cmd)
44
38
  return false unless command_config
45
39
 
40
+ builder = build_option_parser(cmd, command_config)
41
+ execute_command(command_config, builder, args)
42
+ end
43
+
44
+ private
45
+
46
+ # Handle version flag and exit if present
47
+ # @param args [Array<String>] Command line arguments
48
+ def handle_version_flag(args)
49
+ return unless ['-v', '--version'].include?(args[0])
50
+
51
+ puts EpubTools::VERSION
52
+ exit 0
53
+ end
54
+
55
+ # Validate command arguments and exit if invalid
56
+ # @param args [Array<String>] Command line arguments
57
+ def validate_command_args(args)
58
+ commands = registry.available_commands
59
+ return unless args.empty? || !commands.include?(args[0])
60
+
61
+ print_usage(commands)
62
+ exit 1
63
+ end
64
+
65
+ # Build option parser for a command
66
+ # @param cmd [String] Command name
67
+ # @param command_config [Hash] Command configuration
68
+ # @return [OptionBuilder] Configured option builder
69
+ def build_option_parser(cmd, command_config)
46
70
  options = command_config[:default_options].dup
47
71
  required_keys = command_config[:required_keys]
48
72
 
@@ -50,114 +74,40 @@ module EpubTools
50
74
  .with_banner("Usage: #{program_name} #{cmd} [options]")
51
75
  .with_help_option
52
76
 
53
- # Configure command-specific options
54
- configure_command_options(cmd, builder)
77
+ @options_configurator.configure(cmd, builder)
78
+ builder
79
+ end
55
80
 
56
- # Parse arguments and run the command
81
+ # Execute a command with parsed options
82
+ # @param command_config [Hash] Command configuration
83
+ # @param builder [OptionBuilder] Option builder instance
84
+ # @param args [Array<String>] Command line arguments
85
+ # @return [Object] Command instance
86
+ def execute_command(command_config, builder, args)
57
87
  options = builder.parse(args)
58
88
  command_class = command_config[:class]
59
- command_class.new(options).run
60
- true
89
+ command_instance = command_class.new(options)
90
+ command_instance.run
91
+ command_instance
61
92
  end
62
93
 
63
- private
64
-
65
94
  # Print usage information
66
95
  # @param commands [Array<String>] Available commands
67
96
  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
97
+ puts "Usage: #{program_name} COMMAND [options]"
98
+ puts 'Commands:'
99
+ print_command_list
79
100
  end
80
101
 
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', '--oebps-dir DIR', 'EPUB OEBPS directory (required)') do |v|
90
- options[: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
102
+ def print_command_list
103
+ puts ' init Initialize a bare-bones EPUB'
104
+ puts ' extract Extract XHTML files from EPUBs'
105
+ puts ' split Split XHTML into separate XHTMLs per chapter'
106
+ puts ' add Add chapter XHTML files into an EPUB'
107
+ puts ' pack Package an EPUB directory into a .epub file'
108
+ puts ' unpack Unpack an EPUB file into a directory'
109
+ puts ' compile Takes EPUBs in a dir and splits, cleans, and compiles into a single EPUB.'
110
+ puts ' append Extracts and splits EPUBs from a dir and appends them to an existing EPUB.'
161
111
  end
162
112
  end
163
113
  end