epub_tools 0.4.1 → 0.5.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 (51) 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 +124 -0
  5. data/Gemfile +4 -4
  6. data/Gemfile.lock +39 -34
  7. data/Rakefile +2 -0
  8. data/bin/epub-tools +2 -0
  9. data/epub_tools.gemspec +3 -1
  10. data/lib/epub_tools/add_chapters.rb +47 -29
  11. data/lib/epub_tools/chapter_validator.rb +40 -0
  12. data/lib/epub_tools/cli/command_options_configurator.rb +115 -0
  13. data/lib/epub_tools/cli/command_registry.rb +2 -0
  14. data/lib/epub_tools/cli/option_builder.rb +5 -3
  15. data/lib/epub_tools/cli/runner.rb +59 -110
  16. data/lib/epub_tools/cli.rb +16 -29
  17. data/lib/epub_tools/compile_book.rb +48 -65
  18. data/lib/epub_tools/compile_workspace.rb +40 -0
  19. data/lib/epub_tools/epub_configuration.rb +33 -0
  20. data/lib/epub_tools/epub_file_writer.rb +57 -0
  21. data/lib/epub_tools/epub_initializer.rb +83 -162
  22. data/lib/epub_tools/epub_metadata_builder.rb +92 -0
  23. data/lib/epub_tools/loggable.rb +2 -0
  24. data/lib/epub_tools/pack_ebook.rb +28 -14
  25. data/lib/epub_tools/split_chapters.rb +42 -17
  26. data/lib/epub_tools/style_finder.rb +17 -6
  27. data/lib/epub_tools/unpack_ebook.rb +20 -10
  28. data/lib/epub_tools/version.rb +3 -1
  29. data/lib/epub_tools/xhtml_cleaner.rb +1 -0
  30. data/lib/epub_tools/xhtml_extractor.rb +20 -10
  31. data/lib/epub_tools/xhtml_generator.rb +71 -0
  32. data/lib/epub_tools.rb +2 -0
  33. data/test/add_chapters_test.rb +49 -25
  34. data/test/chapter_validator_test.rb +47 -0
  35. data/test/cli/command_registry_test.rb +2 -0
  36. data/test/cli/option_builder_test.rb +24 -14
  37. data/test/cli/runner_test.rb +15 -15
  38. data/test/cli_commands_test.rb +2 -0
  39. data/test/cli_test.rb +2 -0
  40. data/test/cli_version_test.rb +2 -0
  41. data/test/compile_book_test.rb +17 -102
  42. data/test/compile_workspace_test.rb +55 -0
  43. data/test/epub_initializer_test.rb +55 -27
  44. data/test/pack_ebook_test.rb +33 -9
  45. data/test/split_chapters_test.rb +27 -7
  46. data/test/style_finder_test.rb +2 -0
  47. data/test/test_helper.rb +2 -0
  48. data/test/unpack_ebook_test.rb +45 -20
  49. data/test/xhtml_cleaner_test.rb +2 -0
  50. data/test/xhtml_extractor_test.rb +3 -1
  51. metadata +13 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3920ca32a1d5595866904c273c561dceec0021c259708317938e6ee6825adb3f
4
- data.tar.gz: '0490275853356243b7b9ba25367439e6cb2fbee44ac43c71984a9b91f06e2464'
3
+ metadata.gz: 830a3d6d88e106980c19f5c19ea5e0365e39499cc90c0c32ff5c204b7cf24abd
4
+ data.tar.gz: 7898e152850e5e390842e04a56947e2e88623bd1e5b65525561423da7a5ec744
5
5
  SHA512:
6
- metadata.gz: 1c0dfa04ad854e968f2e0ecb1ef3572f598269fedd91278602e0dd3bf524efc2358bab95b800966df175add2b9357ca4d0de366b4d21b30400d599946d299a72
7
- data.tar.gz: df4bfb3cc82a0271ec320d523d41fcb8f36dd2b93d39419d5b719ea1bd24bf089163d80cd408dd6da16482e374567baf003e1e06e2eeee24cff844fcbbf06eaf
6
+ metadata.gz: 7b852a22e630ad9177c5adc692326dac517b904c687e6cbe3874b7e1f43347a53cfdd381cb5b66b7feaf6faafae75129510aac6a23f84f4b3e82734ae84dc8ac
7
+ data.tar.gz: 4602e5922dbdf257436445546b14501a0064861af84fea854feb0a10ed2ef651d6eefb8009ce459c7463c50c3383eabe3220a10d856271ad38c49cc768b8da5f
@@ -6,6 +6,9 @@ on:
6
6
  pull_request:
7
7
  branches: [ main ]
8
8
 
9
+ permissions:
10
+ contents: read
11
+
9
12
  jobs:
10
13
  test:
11
14
  runs-on: ubuntu-latest
data/.rubocop.yml CHANGED
@@ -16,26 +16,19 @@ plugins:
16
16
  - rubocop-minitest
17
17
  - rubocop-rake
18
18
 
19
- Style/FrozenStringLiteralComment:
20
- Enabled: false
21
19
 
22
20
  Metrics/MethodLength:
23
- Enabled: false
21
+ Exclude:
22
+ - 'test/**/*'
24
23
 
25
- Metrics/ClassLength:
26
- Enabled: false
27
-
28
- Metrics/AbcSize:
29
- Enabled: false
30
-
31
- Metrics/CyclomaticComplexity:
32
- Enabled: false
24
+ Naming/PredicateMethod:
25
+ Exclude:
26
+ - 'test/**/*'
33
27
 
34
- Metrics/PerceivedComplexity:
35
- Enabled: false
36
-
37
- Style/OptionalBooleanParameter:
38
- Enabled: false
28
+ Metrics/ClassLength:
29
+ Exclude:
30
+ - 'test/**/*'
39
31
 
40
32
  Minitest/MultipleAssertions:
41
- Enabled: false
33
+ Exclude:
34
+ - 'test/**/*'
data/CLAUDE.md ADDED
@@ -0,0 +1,124 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ EPUB Tools is a Ruby gem and CLI for working with EPUB files. It provides functionality to extract, split, initialize, add chapters, pack, and unpack EPUB books. The project uses a modular architecture with separate classes for each operation and a structured CLI system.
8
+
9
+ ## Development Commands
10
+
11
+ ### Testing
12
+ ```bash
13
+ # Run all tests
14
+ bundle exec rake test
15
+
16
+ # Run a specific test file
17
+ ruby -Itest test/specific_test.rb
18
+ ```
19
+
20
+ ### Linting
21
+ ```bash
22
+ # Run RuboCop linting
23
+ bundle exec rubocop
24
+
25
+ # Fix auto-correctable issues
26
+ bundle exec rubocop --auto-correct
27
+ ```
28
+
29
+ ### Dependencies
30
+ ```bash
31
+ # Install dependencies
32
+ bundle install
33
+
34
+ # Install with documentation dependencies
35
+ bundle install --with doc
36
+ ```
37
+
38
+ ### Documentation
39
+ ```bash
40
+ # Generate and serve YARD documentation
41
+ bundle exec yard server --reload
42
+ # Then visit http://localhost:8808
43
+
44
+ # Generate documentation files
45
+ bundle exec yard doc
46
+ ```
47
+
48
+ ### Gem Management
49
+ ```bash
50
+ # Build the gem
51
+ gem build epub_tools.gemspec
52
+
53
+ # Install locally built gem
54
+ gem install ./epub_tools-*.gem
55
+ ```
56
+
57
+ ## Architecture
58
+
59
+ ### Core Components
60
+
61
+ - **Main Module** (`lib/epub_tools.rb`): Entry point that requires all components
62
+ - **CLI System** (`lib/epub_tools/cli/`): Object-oriented command-line interface
63
+ - `Runner`: Main CLI runner that handles command dispatch
64
+ - `CommandRegistry`: Manages available commands and their configurations
65
+ - `OptionBuilder`: Builds command-line option parsers
66
+ - `CommandOptionsConfigurator`: Handles command-specific option configuration
67
+ - **Core Classes**: Individual operation classes for EPUB manipulation
68
+ - `XHTMLExtractor`: Extracts XHTML files from EPUB archives
69
+ - `SplitChapters`: Splits XHTML files into separate chapters
70
+ - `EpubInitializer`: Creates new EPUB directory structure (uses configuration pattern)
71
+ - `AddChapters`: Adds chapter files to existing EPUB
72
+ - `PackEbook`: Packages EPUB directories into .epub files
73
+ - `UnpackEbook`: Unpacks .epub files into directories
74
+ - `CompileBook`: Full workflow combining multiple operations (uses workspace pattern)
75
+ - **Supporting Classes**: SOLID-designed helper classes
76
+ - `CompileWorkspace`: Manages build directories for CompileBook
77
+ - `ChapterValidator`: Validates chapter sequence completeness
78
+ - `EpubConfiguration`: Configuration object for EPUB initialization
79
+ - `XhtmlGenerator`: Generates XHTML templates for EPUB content
80
+ - `EpubMetadataBuilder`: Builds OPF metadata content
81
+ - `EpubFileWriter`: Handles EPUB file writing operations
82
+
83
+ ### CLI Architecture
84
+
85
+ The CLI uses a registry-based system where:
86
+ 1. Commands are registered in `cli.rb` with their class, required parameters, and defaults
87
+ 2. The `Runner` dispatches to the appropriate command class
88
+ 3. The `CommandOptionsConfigurator` handles command-specific option setup
89
+ 4. Each command class implements a `run` method and uses the `Loggable` mixin for verbose output
90
+
91
+ ### Dependencies
92
+
93
+ - **nokogiri**: XML/HTML parsing for EPUB content
94
+ - **rubyzip**: ZIP file manipulation for EPUB packaging
95
+ - **rake**: Build tasks and testing
96
+ - **minitest**: Testing framework
97
+ - **rubocop**: Code linting with custom configuration
98
+ - **simplecov**: Test coverage reporting
99
+
100
+ ### File Structure
101
+
102
+ - `bin/epub-tools`: Executable CLI entry point
103
+ - `lib/epub_tools/`: Main library code
104
+ - `test/`: Minitest-based test suite
105
+ - `.rubocop.yml`: RuboCop configuration with relaxed complexity rules
106
+ - `epub_tools.gemspec`: Gem specification
107
+ - `Gemfile`: Dependency management
108
+
109
+ ### Testing Patterns
110
+
111
+ Tests use Minitest with:
112
+ - `test_helper.rb` sets up SimpleCov coverage
113
+ - Tests in `test/` directory follow `*_test.rb` naming
114
+ - CLI tests verify command registration and option parsing
115
+ - Individual component tests verify core functionality
116
+
117
+ ### Code Quality
118
+
119
+ The codebase follows SOLID principles with:
120
+ - **Single Responsibility**: Classes have focused, well-defined purposes
121
+ - **Open/Closed**: Extensible design through composition and dependency injection
122
+ - **Dependency Inversion**: Configuration objects and factory patterns
123
+
124
+ RuboCop configuration excludes test files from metrics cops while maintaining strict standards for production code.
data/Gemfile CHANGED
@@ -6,18 +6,18 @@ source 'https://rubygems.org'
6
6
 
7
7
  gem 'nokogiri', '~> 1.18'
8
8
  gem 'rake', '~> 13.2'
9
- gem 'rubyzip', '~> 2.4'
9
+ gem 'rubyzip', '~> 3.2'
10
10
 
11
11
  group :test, :development do
12
- gem 'minitest', '~> 5.25'
12
+ gem 'minitest', '~> 6.0'
13
13
  gem 'rubocop', '~> 1.75', require: false
14
- gem 'rubocop-minitest', '~> 0.38.0', require: false
14
+ gem 'rubocop-minitest', '~> 0.39.0', require: false
15
15
  gem 'rubocop-rake', '~> 0.7.1', require: false
16
16
  gem 'simplecov', require: false
17
17
  end
18
18
 
19
19
  group :doc do
20
- gem 'rdoc', '~> 6.13'
20
+ gem 'rdoc', '~> 7.2'
21
21
  gem 'webrick', '~> 1.9'
22
22
  gem 'yard', '~> 0.9.37'
23
23
  end
data/Gemfile.lock CHANGED
@@ -2,45 +2,49 @@ GEM
2
2
  remote: https://rubygems.org/
3
3
  specs:
4
4
  ast (2.4.3)
5
- date (3.4.1)
5
+ date (3.5.1)
6
6
  docile (1.4.1)
7
- erb (5.0.2)
8
- json (2.13.2)
7
+ drb (2.2.3)
8
+ erb (6.0.2)
9
+ json (2.18.1)
9
10
  language_server-protocol (3.17.0.5)
10
11
  lint_roller (1.1.0)
11
- minitest (5.25.5)
12
- nokogiri (1.18.9-aarch64-linux-gnu)
12
+ minitest (6.0.2)
13
+ drb (~> 2.0)
14
+ prism (~> 1.5)
15
+ nokogiri (1.19.1-aarch64-linux-gnu)
13
16
  racc (~> 1.4)
14
- nokogiri (1.18.9-aarch64-linux-musl)
17
+ nokogiri (1.19.1-aarch64-linux-musl)
15
18
  racc (~> 1.4)
16
- nokogiri (1.18.9-arm-linux-gnu)
19
+ nokogiri (1.19.1-arm-linux-gnu)
17
20
  racc (~> 1.4)
18
- nokogiri (1.18.9-arm-linux-musl)
21
+ nokogiri (1.19.1-arm-linux-musl)
19
22
  racc (~> 1.4)
20
- nokogiri (1.18.9-arm64-darwin)
23
+ nokogiri (1.19.1-arm64-darwin)
21
24
  racc (~> 1.4)
22
- nokogiri (1.18.9-x86_64-darwin)
25
+ nokogiri (1.19.1-x86_64-darwin)
23
26
  racc (~> 1.4)
24
- nokogiri (1.18.9-x86_64-linux-gnu)
27
+ nokogiri (1.19.1-x86_64-linux-gnu)
25
28
  racc (~> 1.4)
26
- nokogiri (1.18.9-x86_64-linux-musl)
29
+ nokogiri (1.19.1-x86_64-linux-musl)
27
30
  racc (~> 1.4)
28
31
  parallel (1.27.0)
29
- parser (3.3.9.0)
32
+ parser (3.3.10.2)
30
33
  ast (~> 2.4.1)
31
34
  racc
32
- prism (1.4.0)
33
- psych (5.2.6)
35
+ prism (1.9.0)
36
+ psych (5.3.1)
34
37
  date
35
38
  stringio
36
39
  racc (1.8.1)
37
40
  rainbow (3.1.1)
38
- rake (13.3.0)
39
- rdoc (6.14.2)
41
+ rake (13.3.1)
42
+ rdoc (7.2.0)
40
43
  erb
41
44
  psych (>= 4.0.0)
42
- regexp_parser (2.11.2)
43
- rubocop (1.79.2)
45
+ tsort
46
+ regexp_parser (2.11.3)
47
+ rubocop (1.84.2)
44
48
  json (~> 2.3)
45
49
  language_server-protocol (~> 3.17.0.2)
46
50
  lint_roller (~> 1.1.0)
@@ -48,13 +52,13 @@ GEM
48
52
  parser (>= 3.3.0.2)
49
53
  rainbow (>= 2.2.2, < 4.0)
50
54
  regexp_parser (>= 2.9.3, < 3.0)
51
- rubocop-ast (>= 1.46.0, < 2.0)
55
+ rubocop-ast (>= 1.49.0, < 2.0)
52
56
  ruby-progressbar (~> 1.7)
53
57
  unicode-display_width (>= 2.4.0, < 4.0)
54
- rubocop-ast (1.46.0)
58
+ rubocop-ast (1.49.0)
55
59
  parser (>= 3.3.7.2)
56
- prism (~> 1.4)
57
- rubocop-minitest (0.38.1)
60
+ prism (~> 1.7)
61
+ rubocop-minitest (0.39.1)
58
62
  lint_roller (~> 1.1)
59
63
  rubocop (>= 1.75.0, < 2.0)
60
64
  rubocop-ast (>= 1.38.0, < 2.0)
@@ -62,19 +66,20 @@ GEM
62
66
  lint_roller (~> 1.1)
63
67
  rubocop (>= 1.72.1)
64
68
  ruby-progressbar (1.13.0)
65
- rubyzip (2.4.1)
69
+ rubyzip (3.2.2)
66
70
  simplecov (0.22.0)
67
71
  docile (~> 1.1)
68
72
  simplecov-html (~> 0.11)
69
73
  simplecov_json_formatter (~> 0.1)
70
74
  simplecov-html (0.13.2)
71
75
  simplecov_json_formatter (0.1.4)
72
- stringio (3.1.7)
73
- unicode-display_width (3.1.5)
74
- unicode-emoji (~> 4.0, >= 4.0.4)
75
- unicode-emoji (4.0.4)
76
- webrick (1.9.1)
77
- yard (0.9.37)
76
+ stringio (3.2.0)
77
+ tsort (0.2.0)
78
+ unicode-display_width (3.2.0)
79
+ unicode-emoji (~> 4.1)
80
+ unicode-emoji (4.2.0)
81
+ webrick (1.9.2)
82
+ yard (0.9.38)
78
83
 
79
84
  PLATFORMS
80
85
  aarch64-linux-gnu
@@ -87,14 +92,14 @@ PLATFORMS
87
92
  x86_64-linux-musl
88
93
 
89
94
  DEPENDENCIES
90
- minitest (~> 5.25)
95
+ minitest (~> 6.0)
91
96
  nokogiri (~> 1.18)
92
97
  rake (~> 13.2)
93
- rdoc (~> 6.13)
98
+ rdoc (~> 7.2)
94
99
  rubocop (~> 1.75)
95
- rubocop-minitest (~> 0.38.0)
100
+ rubocop-minitest (~> 0.39.0)
96
101
  rubocop-rake (~> 0.7.1)
97
- rubyzip (~> 2.4)
102
+ rubyzip (~> 3.2)
98
103
  simplecov
99
104
  webrick (~> 1.9)
100
105
  yard (~> 0.9.37)
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rake/testtask'
2
4
 
3
5
  desc 'Run all tests'
data/bin/epub-tools CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
2
4
  require_relative '../lib/epub_tools'
3
5
 
4
6
  # Use the new object-oriented CLI architecture
data/epub_tools.gemspec CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'lib/epub_tools/version'
2
4
 
3
5
  Gem::Specification.new do |spec|
@@ -20,5 +22,5 @@ Gem::Specification.new do |spec|
20
22
 
21
23
  spec.add_dependency 'nokogiri', '~> 1.18'
22
24
  spec.add_dependency 'rake', '~> 13.2'
23
- spec.add_dependency 'rubyzip', '~> 2.4'
25
+ spec.add_dependency 'rubyzip', '~> 3.2'
24
26
  end
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
2
4
  require 'nokogiri'
3
5
  require 'fileutils'
4
6
  require_relative 'loggable'
@@ -7,6 +9,7 @@ module EpubTools
7
9
  # Moves new chapters into an unpacked EPUB
8
10
  class AddChapters
9
11
  include Loggable
12
+
10
13
  # Initializes the class
11
14
  # @param options [Hash] Configuration options
12
15
  # @option options [String] :chapters_dir Directory from which to move the xhtml chapters.
@@ -80,24 +83,7 @@ module EpubTools
80
83
  manifest = doc.at_xpath('//xmlns:manifest')
81
84
  spine = doc.at_xpath('//xmlns:spine')
82
85
 
83
- filenames.each do |filename|
84
- id = chapter_id(filename)
85
- # Add <item> to the manifest if missing
86
- unless doc.at_xpath("//xmlns:item[@href='#{filename}']")
87
- item = Nokogiri::XML::Node.new('item', doc)
88
- item['id'] = id
89
- item['href'] = filename
90
- item['media-type'] = 'application/xhtml+xml'
91
- manifest.add_child(item)
92
- end
93
-
94
- # Add <itemref> to the spine if missing
95
- next if doc.at_xpath("//xmlns:itemref[@idref='#{id}']")
96
-
97
- itemref = Nokogiri::XML::Node.new('itemref', doc)
98
- itemref['idref'] = id
99
- spine.add_child(itemref)
100
- end
86
+ filenames.each { |filename| update_opf_for_file(doc, manifest, spine, filename) }
101
87
 
102
88
  File.write(@opf_file, doc.to_xml(indent: 2))
103
89
  end
@@ -106,19 +92,51 @@ module EpubTools
106
92
  doc = Nokogiri::XML(File.read(@nav_file)) { |config| config.default_xml.noblanks }
107
93
  nav = doc.at_xpath('//xmlns:nav[@epub:type="toc"]/xmlns:ol')
108
94
 
109
- filenames.each do |filename|
110
- # Create a new <li><a href="...">Label</a></li> element
111
- label = File.basename(filename, '.xhtml').gsub('_', ' ').capitalize
112
- label = 'Prologue' if label == 'Chapter 0'
113
- li = Nokogiri::XML::Node.new('li', doc)
114
- a = Nokogiri::XML::Node.new('a', doc)
115
- a['href'] = filename
116
- a.content = label
117
- li.add_child(a)
118
- nav.add_child(li)
119
- end
95
+ filenames.each { |filename| nav.add_child(create_nav_link(doc, filename)) }
120
96
 
121
97
  File.write(@nav_file, doc.to_xml(indent: 2))
122
98
  end
99
+
100
+ def create_nav_link(doc, filename)
101
+ li = Nokogiri::XML::Node.new('li', doc)
102
+ a = Nokogiri::XML::Node.new('a', doc)
103
+ a['href'] = filename
104
+ a.content = format_chapter_label(filename)
105
+ li.add_child(a)
106
+ li
107
+ end
108
+
109
+ def format_chapter_label(filename)
110
+ label = File.basename(filename, '.xhtml').gsub('_', ' ').capitalize
111
+ label == 'Chapter 0' ? 'Prologue' : label
112
+ end
113
+
114
+ def update_opf_for_file(doc, manifest, spine, filename)
115
+ id = chapter_id(filename)
116
+ add_manifest_item(doc, manifest, filename, id) unless manifest_item_exists?(doc, filename)
117
+ add_spine_itemref(doc, spine, id) unless spine_itemref_exists?(doc, id)
118
+ end
119
+
120
+ def manifest_item_exists?(doc, filename)
121
+ doc.at_xpath("//xmlns:item[@href='#{filename}']")
122
+ end
123
+
124
+ def spine_itemref_exists?(doc, id)
125
+ doc.at_xpath("//xmlns:itemref[@idref='#{id}']")
126
+ end
127
+
128
+ def add_manifest_item(doc, manifest, filename, id)
129
+ item = Nokogiri::XML::Node.new('item', doc)
130
+ item['id'] = id
131
+ item['href'] = filename
132
+ item['media-type'] = 'application/xhtml+xml'
133
+ manifest.add_child(item)
134
+ end
135
+
136
+ def add_spine_itemref(doc, spine, id)
137
+ itemref = Nokogiri::XML::Node.new('itemref', doc)
138
+ itemref['idref'] = id
139
+ spine.add_child(itemref)
140
+ end
123
141
  end
124
142
  end
@@ -0,0 +1,40 @@
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
+ def validate
16
+ log 'Validating chapter sequence...'
17
+ nums = extract_chapter_numbers
18
+ check_sequence_completeness(nums)
19
+ log "Chapter sequence is complete: #{nums.first} to #{nums.last}."
20
+ end
21
+
22
+ private
23
+
24
+ def extract_chapter_numbers
25
+ nums = Dir.glob(File.join(@chapters_dir, '*.xhtml')).map do |file|
26
+ if (m = File.basename(file, '.xhtml').match(/_(\d+)\z/))
27
+ m[1].to_i
28
+ end
29
+ end.compact
30
+ raise "No chapter files found in #{@chapters_dir}" if nums.empty?
31
+
32
+ nums.sort.uniq
33
+ end
34
+
35
+ def check_sequence_completeness(sorted)
36
+ missing = (sorted.first..sorted.last).to_a - sorted
37
+ raise "Missing chapter numbers: #{missing.join(' ')}" if missing.any?
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,115 @@
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 'compile' command
100
+ # @param builder [OptionBuilder] Option builder instance
101
+ def configure_compile_options(builder)
102
+ builder.with_title_option
103
+ .with_author_option
104
+ .with_custom_options do |opts, options|
105
+ opts.on('-s DIR', '--source-dir DIR', 'Directory with EPUBs to extract XHTMLs from (required)') do |v|
106
+ options[:source_dir] = v
107
+ end
108
+ opts.on('-o FILE', '--output FILE', 'EPUB to create (default: book title in source dir)') do |v|
109
+ options[:output_file] = v
110
+ end
111
+ end.with_cover_option.with_verbose_option
112
+ end
113
+ end
114
+ end
115
+ 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