epub_tools 0.3.1 → 0.4.1

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.rubocop.yml +41 -0
  4. data/.tool-versions +1 -0
  5. data/Gemfile +13 -9
  6. data/Gemfile.lock +63 -12
  7. data/README.md +26 -6
  8. data/bin/epub-tools +3 -109
  9. data/epub_tools.gemspec +6 -8
  10. data/lib/epub_tools/add_chapters.rb +41 -19
  11. data/lib/epub_tools/cli/command_registry.rb +47 -0
  12. data/lib/epub_tools/cli/option_builder.rb +164 -0
  13. data/lib/epub_tools/cli/runner.rb +164 -0
  14. data/lib/epub_tools/cli.rb +45 -0
  15. data/lib/epub_tools/compile_book.rb +60 -28
  16. data/lib/epub_tools/epub_initializer.rb +38 -28
  17. data/lib/epub_tools/loggable.rb +11 -0
  18. data/lib/epub_tools/pack_ebook.rb +18 -12
  19. data/lib/epub_tools/split_chapters.rb +31 -21
  20. data/lib/epub_tools/{text_style_class_finder.rb → style_finder.rb} +21 -17
  21. data/lib/epub_tools/unpack_ebook.rb +17 -12
  22. data/lib/epub_tools/version.rb +1 -1
  23. data/lib/epub_tools/xhtml_cleaner.rb +17 -13
  24. data/lib/epub_tools/xhtml_extractor.rb +20 -11
  25. data/lib/epub_tools.rb +2 -1
  26. data/test/add_chapters_test.rb +12 -5
  27. data/test/cli/command_registry_test.rb +66 -0
  28. data/test/cli/option_builder_test.rb +173 -0
  29. data/test/cli/runner_test.rb +91 -0
  30. data/test/cli_commands_test.rb +100 -0
  31. data/test/cli_test.rb +4 -0
  32. data/test/cli_version_test.rb +5 -3
  33. data/test/compile_book_test.rb +11 -2
  34. data/test/epub_initializer_test.rb +51 -31
  35. data/test/pack_ebook_test.rb +14 -8
  36. data/test/split_chapters_test.rb +22 -1
  37. data/test/{text_style_class_finder_test.rb → style_finder_test.rb} +7 -6
  38. data/test/test_helper.rb +4 -5
  39. data/test/unpack_ebook_test.rb +21 -5
  40. data/test/xhtml_cleaner_test.rb +13 -7
  41. data/test/xhtml_extractor_test.rb +17 -1
  42. metadata +21 -38
  43. data/.ruby-version +0 -1
  44. data/lib/epub_tools/cli_helper.rb +0 -31
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 815d8970acc779f70f7d86395a3658a296536675539dfcf3e7f9a0e0f2d4153a
4
- data.tar.gz: da9131f5868b8e8c438e9f41c526d62a2d9f70a320e8d20fa14ffcf4a487f22c
3
+ metadata.gz: 3920ca32a1d5595866904c273c561dceec0021c259708317938e6ee6825adb3f
4
+ data.tar.gz: '0490275853356243b7b9ba25367439e6cb2fbee44ac43c71984a9b91f06e2464'
5
5
  SHA512:
6
- metadata.gz: a7dc1015fc79f4c72edec8fb672622122a3540ef3b8923683777a178d397fff5af4e0b0765f237e748ca7a103b9e4c2c0c0c0902d180fcd9cb87e90c613c862f
7
- data.tar.gz: fe60fa02c6a7db816b7aab2f5fe368de1a463fdaaa5aa1892146281362ea2cda49d904da541b67e34b2329225e01d9fcc7cd9a3a4c15503cb06cf3d6a10e26ec
6
+ metadata.gz: 1c0dfa04ad854e968f2e0ecb1ef3572f598269fedd91278602e0dd3bf524efc2358bab95b800966df175add2b9357ca4d0de366b4d21b30400d599946d299a72
7
+ data.tar.gz: df4bfb3cc82a0271ec320d523d41fcb8f36dd2b93d39419d5b719ea1bd24bf089163d80cd408dd6da16482e374567baf003e1e06e2eeee24cff844fcbbf06eaf
data/.gitignore CHANGED
@@ -7,3 +7,5 @@ vendor/bundle/
7
7
  *.gem
8
8
 
9
9
  text_style_classes.yaml
10
+
11
+ **/.claude/
data/.rubocop.yml ADDED
@@ -0,0 +1,41 @@
1
+ # The behavior of RuboCop can be controlled via the .rubocop.yml
2
+ # configuration file. It makes it possible to enable/disable
3
+ # certain cops (checks) and to alter their behavior if they accept
4
+ # any parameters. The file can be placed either in your home
5
+ # directory or in some project directory.
6
+ #
7
+ # RuboCop will start looking for the configuration file in the directory
8
+ # where the inspected file is and continue its way up to the root directory.
9
+ #
10
+ # See https://docs.rubocop.org/rubocop/configuration
11
+
12
+ AllCops:
13
+ NewCops: enable
14
+
15
+ plugins:
16
+ - rubocop-minitest
17
+ - rubocop-rake
18
+
19
+ Style/FrozenStringLiteralComment:
20
+ Enabled: false
21
+
22
+ Metrics/MethodLength:
23
+ Enabled: false
24
+
25
+ Metrics/ClassLength:
26
+ Enabled: false
27
+
28
+ Metrics/AbcSize:
29
+ Enabled: false
30
+
31
+ Metrics/CyclomaticComplexity:
32
+ Enabled: false
33
+
34
+ Metrics/PerceivedComplexity:
35
+ Enabled: false
36
+
37
+ Style/OptionalBooleanParameter:
38
+ Enabled: false
39
+
40
+ Minitest/MultipleAssertions:
41
+ Enabled: false
data/.tool-versions ADDED
@@ -0,0 +1 @@
1
+ ruby 3.4.5
data/Gemfile CHANGED
@@ -2,18 +2,22 @@
2
2
 
3
3
  ruby '>= 3.2'
4
4
 
5
- source "https://rubygems.org"
5
+ source 'https://rubygems.org'
6
6
 
7
- gem "nokogiri", "~> 1.18"
8
- gem "rake", "~> 13.2"
9
- gem "rubyzip", "~> 2.4"
7
+ gem 'nokogiri', '~> 1.18'
8
+ gem 'rake', '~> 13.2'
9
+ gem 'rubyzip', '~> 2.4'
10
10
 
11
- group :test do
12
- gem "minitest", "~> 5.25"
13
- gem "simplecov", require: false
11
+ group :test, :development do
12
+ gem 'minitest', '~> 5.25'
13
+ gem 'rubocop', '~> 1.75', require: false
14
+ gem 'rubocop-minitest', '~> 0.38.0', require: false
15
+ gem 'rubocop-rake', '~> 0.7.1', require: false
16
+ gem 'simplecov', require: false
14
17
  end
15
18
 
16
19
  group :doc do
17
- gem "yard", "~> 0.9.37"
20
+ gem 'rdoc', '~> 6.13'
21
+ gem 'webrick', '~> 1.9'
22
+ gem 'yard', '~> 0.9.37'
18
23
  end
19
-
data/Gemfile.lock CHANGED
@@ -1,33 +1,79 @@
1
1
  GEM
2
2
  remote: https://rubygems.org/
3
3
  specs:
4
+ ast (2.4.3)
5
+ date (3.4.1)
4
6
  docile (1.4.1)
7
+ erb (5.0.2)
8
+ json (2.13.2)
9
+ language_server-protocol (3.17.0.5)
10
+ lint_roller (1.1.0)
5
11
  minitest (5.25.5)
6
- nokogiri (1.18.8-aarch64-linux-gnu)
12
+ nokogiri (1.18.9-aarch64-linux-gnu)
7
13
  racc (~> 1.4)
8
- nokogiri (1.18.8-aarch64-linux-musl)
14
+ nokogiri (1.18.9-aarch64-linux-musl)
9
15
  racc (~> 1.4)
10
- nokogiri (1.18.8-arm-linux-gnu)
16
+ nokogiri (1.18.9-arm-linux-gnu)
11
17
  racc (~> 1.4)
12
- nokogiri (1.18.8-arm-linux-musl)
18
+ nokogiri (1.18.9-arm-linux-musl)
13
19
  racc (~> 1.4)
14
- nokogiri (1.18.8-arm64-darwin)
20
+ nokogiri (1.18.9-arm64-darwin)
15
21
  racc (~> 1.4)
16
- nokogiri (1.18.8-x86_64-darwin)
22
+ nokogiri (1.18.9-x86_64-darwin)
17
23
  racc (~> 1.4)
18
- nokogiri (1.18.8-x86_64-linux-gnu)
24
+ nokogiri (1.18.9-x86_64-linux-gnu)
19
25
  racc (~> 1.4)
20
- nokogiri (1.18.8-x86_64-linux-musl)
26
+ nokogiri (1.18.9-x86_64-linux-musl)
21
27
  racc (~> 1.4)
28
+ parallel (1.27.0)
29
+ parser (3.3.9.0)
30
+ ast (~> 2.4.1)
31
+ racc
32
+ prism (1.4.0)
33
+ psych (5.2.6)
34
+ date
35
+ stringio
22
36
  racc (1.8.1)
23
- rake (13.2.1)
37
+ rainbow (3.1.1)
38
+ rake (13.3.0)
39
+ rdoc (6.14.2)
40
+ erb
41
+ psych (>= 4.0.0)
42
+ regexp_parser (2.11.2)
43
+ rubocop (1.79.2)
44
+ json (~> 2.3)
45
+ language_server-protocol (~> 3.17.0.2)
46
+ lint_roller (~> 1.1.0)
47
+ parallel (~> 1.10)
48
+ parser (>= 3.3.0.2)
49
+ rainbow (>= 2.2.2, < 4.0)
50
+ regexp_parser (>= 2.9.3, < 3.0)
51
+ rubocop-ast (>= 1.46.0, < 2.0)
52
+ ruby-progressbar (~> 1.7)
53
+ unicode-display_width (>= 2.4.0, < 4.0)
54
+ rubocop-ast (1.46.0)
55
+ parser (>= 3.3.7.2)
56
+ prism (~> 1.4)
57
+ rubocop-minitest (0.38.1)
58
+ lint_roller (~> 1.1)
59
+ rubocop (>= 1.75.0, < 2.0)
60
+ rubocop-ast (>= 1.38.0, < 2.0)
61
+ rubocop-rake (0.7.1)
62
+ lint_roller (~> 1.1)
63
+ rubocop (>= 1.72.1)
64
+ ruby-progressbar (1.13.0)
24
65
  rubyzip (2.4.1)
25
66
  simplecov (0.22.0)
26
67
  docile (~> 1.1)
27
68
  simplecov-html (~> 0.11)
28
69
  simplecov_json_formatter (~> 0.1)
29
- simplecov-html (0.13.1)
70
+ simplecov-html (0.13.2)
30
71
  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)
31
77
  yard (0.9.37)
32
78
 
33
79
  PLATFORMS
@@ -44,12 +90,17 @@ DEPENDENCIES
44
90
  minitest (~> 5.25)
45
91
  nokogiri (~> 1.18)
46
92
  rake (~> 13.2)
93
+ rdoc (~> 6.13)
94
+ rubocop (~> 1.75)
95
+ rubocop-minitest (~> 0.38.0)
96
+ rubocop-rake (~> 0.7.1)
47
97
  rubyzip (~> 2.4)
48
98
  simplecov
99
+ webrick (~> 1.9)
49
100
  yard (~> 0.9.37)
50
101
 
51
102
  RUBY VERSION
52
- ruby 3.4.3p32
103
+ ruby 3.4.5p51
53
104
 
54
105
  BUNDLED WITH
55
- 2.6.7
106
+ 2.7.0
data/README.md CHANGED
@@ -1,16 +1,20 @@
1
1
  # EPUB Tools
2
2
 
3
- [![Build Status](https://github.com/jaimerodas/epub_tools/actions/workflows/ci.yml/badge.svg)](https://github.com/jaimerodas/epub_tools/actions) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
3
+ [![Build Status](https://github.com/jaimerodas/epub_tools/actions/workflows/ci.yml/badge.svg)](https://github.com/jaimerodas/epub_tools/actions) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Gem Version](https://badge.fury.io/rb/epub_tools.svg)](https://badge.fury.io/rb/epub_tools)
4
4
 
5
5
  **TL;DR:** A Ruby gem and CLI for working with EPUB files: extract, split, initialize, add chapters, pack, and unpack EPUB books.
6
6
 
7
7
  ## Installation
8
- Install the gem via RubyGems:
8
+
9
+ ### Requirements
10
+ - Ruby 3.2 or higher
11
+
12
+ ### Install from RubyGems
9
13
  ```bash
10
14
  gem install epub_tools
11
15
  ```
12
16
 
13
- Or build and install locally:
17
+ ### Build and install locally
14
18
  ```bash
15
19
  bundle install
16
20
  gem build epub_tools.gemspec
@@ -101,7 +105,7 @@ EpubTools::UnpackEbook.new('MyBook.epub', 'unpacked_dir').run
101
105
  ## Development & Testing
102
106
  Clone the repo and install dependencies:
103
107
  ```bash
104
- git clone <repo-url>
108
+ git clone https://github.com/jaimerodas/epub_tools.git
105
109
  cd epub_tools
106
110
  bundle install
107
111
  ```
@@ -111,9 +115,25 @@ Run tests:
111
115
  bundle exec rake test
112
116
  ```
113
117
 
114
- Enable coverage reporting:
118
+ Run linting (RuboCop):
119
+ ```bash
120
+ bundle exec rubocop
121
+ ```
122
+ ## Documentation
123
+
124
+ Detailed API documentation can be generated using YARD. To view the docs locally, serve the documentation locally with YARD:
125
+
126
+ ```bash
127
+ bundle exec yard server --reload
128
+ ```
129
+
130
+ Then navigate to http://localhost:8808 in your browser.
131
+
132
+ To (re)generate the documentation, install the documentation dependencies and run:
133
+
115
134
  ```bash
116
- COVERAGE=true bundle exec rake test
135
+ bundle install --with doc
136
+ bundle exec yard doc
117
137
  ```
118
138
 
119
139
  ## Contributing
data/bin/epub-tools CHANGED
@@ -1,112 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  require_relative '../lib/epub_tools'
3
3
 
4
- prog = File.basename($PROGRAM_NAME)
5
- # Global version flag: print version and exit if invoked as `epub-tools -v/--version`
6
- if ARGV[0] == '-v' || ARGV[0] == '--version'
7
- puts EpubTools::VERSION
8
- exit 0
9
- end
10
- script_dir = File.expand_path(File.join(__dir__, '..'))
11
- commands = %w[add extract init split pack unpack compile]
12
-
13
- if ARGV.empty? || !commands.include?(ARGV[0])
14
- puts <<~USAGE
15
- Usage: #{prog} COMMAND [options]
16
- Commands:
17
- init Initialize a bare-bones EPUB
18
- extract Extract XHTML files from EPUBs
19
- split Split XHTML into separate XHTMLs per chapter
20
- add Add chapter XHTML files into an EPUB
21
- pack Package an EPUB directory into a .epub file
22
- unpack Unpack an EPUB file into a directory
23
- compile Takes EPUBs in a dir and splits, cleans, and compiles into a single EPUB.
24
- USAGE
25
- exit 1
26
- end
27
-
28
- cmd = ARGV.shift
29
-
30
- case cmd
31
- when 'add'
32
- options = {}
33
- EpubTools::CLIHelper.parse(options, [:chapters_dir, :epub_oebps_dir]) do |opts, o|
34
- opts.banner = "Usage: #{prog} add [options]"
35
- opts.on('-c DIR', '--chapters-dir DIR', 'Chapters directory (required)') { |v| o[:chapters_dir] = v }
36
- opts.on('-e DIR', '--epub-oebps-dir DIR', 'EPUB OEBPS directory (required)') { |v| o[:epub_oebps_dir] = v }
37
- end
38
-
39
- EpubTools::AddChapters.new(options[:chapters_dir], options[:epub_oebps_dir]).run
40
-
41
- when 'extract'
42
- options = { verbose: true }
43
- EpubTools::CLIHelper.parse(options, [:source_dir, :target_dir]) do |opts, o|
44
- opts.banner = "Usage: #{prog} extract [options]"
45
- opts.on('-s DIR', '--source-dir DIR', 'Directory with EPUBs to extract XHTMLs from (required)') { |v| o[:source_dir] = v }
46
- opts.on('-t DIR', '--target-dir DIR', 'Directory where the XHTML files will be extracted to (required)') { |v| o[:target_dir] = v }
47
- opts.on('-q', '--quiet', 'Run quietly (default: verbose)') { |v| o[:verbose] = !v }
48
- end
49
- EpubTools::XHTMLExtractor.new(
50
- source_dir: options[:source_dir],
51
- target_dir: options[:target_dir],
52
- verbose: options[:verbose]
53
- ).extract_all
54
-
55
- when 'split'
56
- options = { output_dir: './chapters', prefix: 'chapter', verbose: true }
57
- EpubTools::CLIHelper.parse(options, [:input_file, :book_title]) do |opts, o|
58
- opts.banner = "Usage: #{prog} split [options]"
59
- opts.on('-i FILE', '--input FILE', 'Source XHTML file (required)') { |v| options[:input_file] = v }
60
- opts.on('-t TITLE', '--title TITLE', 'Book title for HTML <title> tags (required)') { |v| options[:book_title] = v }
61
- opts.on('-o DIR', '--output-dir DIR', "Output directory for chapter files (default: #{options[:output_dir]})") { |v| options[:output_dir] = v }
62
- opts.on('-p PREFIX', '--prefix PREFIX', "Filename prefix for chapters (default: #{options[:prefix]})") { |v| options[:prefix] = v }
63
- opts.on('-q', '--quiet', 'Run quietly (default: verbose)') { |v| o[:verbose] = !v }
64
- end
65
- EpubTools::SplitChapters.new(options[:input_file], options[:book_title], options[:output_dir], options[:prefix], options[:verbose]).run
66
-
67
- when 'init'
68
- options = {}
69
- EpubTools::CLIHelper.parse(options, [:title, :author, :destination]) do |opts, o|
70
- opts.banner = "Usage: #{prog} init [options]"
71
- opts.on('-t TITLE', '--title TITLE', 'Book title (required)') { |v| o[:title] = v }
72
- opts.on('-a AUTHOR', '--author AUTHOR', 'Author name (required)') { |v| o[:author] = v }
73
- opts.on('-o DIR', '--output-dir DIR', 'Destination EPUB directory (required)') { |v| o[:destination] = v }
74
- opts.on('-c PATH', '--cover PATH', 'Cover image file path (optional)') { |v| o[:cover_image] = v }
75
- end
76
-
77
- EpubTools::EpubInitializer.new(options[:title], options[:author], options[:destination], options[:cover_image]).run
78
-
79
- when 'pack'
80
- options = {verbose: true}
81
- EpubTools::CLIHelper.parse(options, [:input_dir, :output_file]) do |opts, o|
82
- opts.banner = "Usage: #{prog} pack [options]"
83
- opts.on('-i DIR', '--input-dir DIR', 'EPUB directory to package (required)') { |v| o[:input_dir] = v }
84
- opts.on('-o FILE', '--output-file FILE', 'Output EPUB file path (required)') { |v| o[:output_file] = v }
85
- opts.on('-q', '--quiet', 'Run quietly (default: verbose)') { |v| o[:verbose] = !v }
86
- end
87
-
88
- EpubTools::PackEbook.new(options[:input_dir], options[:output_file], verbose: options[:verbose]).run
89
-
90
- when 'unpack'
91
- options = {verbose: true}
92
- EpubTools::CLIHelper.parse(options, [:epub_file]) do |opts, o|
93
- opts.banner = "Usage: #{prog} unpack [options]"
94
- opts.on('-i FILE', '--input-file FILE', 'EPUB file to unpack (required)') { |v| o[:epub_file] = v }
95
- opts.on('-o DIR', '--output-dir DIR', 'Output directory to extract into (default: basename of epub)') { |v| o[:output_dir] = v }
96
- opts.on('-q', '--quiet', 'Run quietly (default: verbose)') { |v| o[:verbose] = !v }
97
- end
98
- EpubTools::UnpackEbook.new(options[:epub_file], options[:output_dir], verbose: options[:verbose]).run
99
-
100
- when 'compile'
101
- options = {verbose: true}
102
- EpubTools::CLIHelper.parse(options, %i(title author source_dir)) do |opts, o|
103
- opts.banner = "Usage: #{prog} compile [options]"
104
- opts.on('-t TITLE', '--title TITLE', 'Book title (required)') { |v| o[:title] = v }
105
- opts.on('-a AUTHOR', '--author AUTHOR', 'Author name (required)') { |v| o[:author] = v }
106
- opts.on('-s DIR', '--source-dir DIR', 'Directory with EPUBs to extract XHTMLs from (required)') { |v| o[:source_dir] = v }
107
- opts.on('-o FILE', '--output FILE', 'EPUB to create (default: book title in source dir)') { |v| o[:output_file] = v }
108
- opts.on('-c PATH', '--cover PATH', 'Cover image file path (optional)') { |v| o[:cover_image] = v }
109
- opts.on('-q', '--quiet', 'Run quietly (default: verbose)') { |v| o[:verbose] = !v }
110
- end
111
- EpubTools::CompileBook.new(**options).run
112
- end
4
+ # Use the new object-oriented CLI architecture
5
+ runner = EpubTools::CLI.create_runner
6
+ runner.run(ARGV)
data/epub_tools.gemspec CHANGED
@@ -11,16 +11,14 @@ Gem::Specification.new do |spec|
11
11
  spec.files = `git ls-files`.split("\n")
12
12
  spec.require_paths = ['lib']
13
13
  spec.executables = ['epub-tools']
14
- spec.required_ruby_version = ">= 3.2"
15
- spec.metadata = {
16
- "source_code_uri" => "https://github.com/jaimerodas/epub_tools/tree/main",
17
- "homepage_uri" => "https://github.com/jaimerodas/epub_tools"
14
+ spec.required_ruby_version = '>= 3.2'
15
+ spec.metadata = {
16
+ 'source_code_uri' => 'https://github.com/jaimerodas/epub_tools/tree/main',
17
+ 'homepage_uri' => 'https://github.com/jaimerodas/epub_tools',
18
+ 'rubygems_mfa_required' => 'true'
18
19
  }
19
20
 
20
21
  spec.add_dependency 'nokogiri', '~> 1.18'
21
- spec.add_dependency 'rubyzip', '~> 2.4'
22
22
  spec.add_dependency 'rake', '~> 13.2'
23
-
24
- spec.add_development_dependency 'minitest', '~> 5.25'
25
- spec.add_development_dependency 'simplecov', '~> 0'
23
+ spec.add_dependency 'rubyzip', '~> 2.4'
26
24
  end
@@ -1,24 +1,29 @@
1
1
  #!/usr/bin/env ruby
2
2
  require 'nokogiri'
3
3
  require 'fileutils'
4
+ require_relative 'loggable'
4
5
 
5
6
  module EpubTools
6
7
  # Moves new chapters into an unpacked EPUB
7
8
  class AddChapters
8
- # :args: chapters_dir, epub_dir, verbose:
9
- # [chapters_dir] Directory from which to move the xhtml chapters. It assumes the
10
- # directory will contain one or more files named +chapter_XX.xhtml+,
11
- # where +XX+ is a number. Defaults to +./chapters+.
12
- # [epub_dir] Unpacked EPUB directory to move the chapters to. It should be the same
13
- # directory that contains the +package.opf+ and +nav.xhtml+ files. Defaults
14
- # to +./epub/OEBPS+.
15
- # [verbose:] Whether to log progress to +STDOUT+ or not. Defaults to +false+.
16
- def initialize(chapters_dir = './chapters', epub_dir = './epub/OEBPS', verbose = false)
17
- @chapters_dir = chapters_dir
18
- @epub_dir = epub_dir
9
+ include Loggable
10
+ # Initializes the class
11
+ # @param options [Hash] Configuration options
12
+ # @option options [String] :chapters_dir Directory from which to move the xhtml chapters.
13
+ # It assumes the directory will contain one or more files named
14
+ # +chapter_XX.xhtml+, where +XX+ is a number. (default: './chapters')
15
+ # @option options [String] :epub_dir Unpacked EPUB directory to move the chapters to. It should
16
+ # be the same directory that contains the +package.opf+ and +nav.xhtml+
17
+ # files. (default: './epub/OEBPS')
18
+ # @option options [Boolean] :verbose Whether to log progress to STDOUT (default: false)
19
+ def initialize(options = {})
20
+ @chapters_dir = File.expand_path(options[:chapters_dir] || './chapters')
21
+ @epub_dir = File.expand_path(options[:oebps_dir] || './epub/OEBPS')
19
22
  @opf_file = File.join(@epub_dir, 'package.opf')
20
23
  @nav_file = File.join(@epub_dir, 'nav.xhtml')
21
- @verbose = verbose
24
+ @verbose = options[:verbose] || false
25
+
26
+ validate_directories!
22
27
  end
23
28
 
24
29
  # It works like this:
@@ -27,21 +32,38 @@ module EpubTools
27
32
  # It will sort the files by extracting the chapter number.
28
33
  # - Finally, it will update the +nav.xhtml+ file with the new chapters. Note that if there's a
29
34
  # file named +chapter_0.xhtml+, it will be added to the +nav.xhtml+ as the Prologue.
35
+ # @return [Array<String>] List of moved chapter filenames
30
36
  def run
31
37
  moved_files = move_chapters
32
38
  update_package_opf(moved_files)
33
39
  update_nav_xhtml(moved_files)
34
- @verbose ? moved_files.each {|f| puts "Moved: #{f}"} : moved_files
40
+ moved_files.each { |f| log("Moved: #{f}") }
41
+ moved_files
35
42
  end
36
43
 
37
44
  private
38
45
 
46
+ def validate_directories!
47
+ raise ArgumentError, "Chapters directory '#{@chapters_dir}' does not exist" unless Dir.exist?(@chapters_dir)
48
+
49
+ raise ArgumentError, "EPUB directory '#{@epub_dir}' does not exist" unless Dir.exist?(@epub_dir)
50
+
51
+ raise ArgumentError, "EPUB package.opf file missing at '#{@opf_file}'" unless File.exist?(@opf_file)
52
+
53
+ return if File.exist?(@nav_file)
54
+
55
+ raise ArgumentError, "EPUB nav.xhtml file missing at '#{@nav_file}'"
56
+ end
57
+
39
58
  def move_chapters
40
59
  # Sort by chapter number (numeric)
41
60
  chapter_files = Dir.glob(File.join(@chapters_dir, '*.xhtml')).sort_by do |path|
42
61
  # extract first integer from filename (e.g. chapter_10.xhtml -> 10)
43
62
  File.basename(path)[/\d+/].to_i
44
63
  end
64
+
65
+ raise ArgumentError, "No .xhtml files found in '#{@chapters_dir}'" if chapter_files.empty?
66
+
45
67
  chapter_files.each do |file|
46
68
  FileUtils.mv(file, @epub_dir)
47
69
  end
@@ -70,11 +92,11 @@ module EpubTools
70
92
  end
71
93
 
72
94
  # Add <itemref> to the spine if missing
73
- unless doc.at_xpath("//xmlns:itemref[@idref='#{id}']")
74
- itemref = Nokogiri::XML::Node.new('itemref', doc)
75
- itemref['idref'] = id
76
- spine.add_child(itemref)
77
- end
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)
78
100
  end
79
101
 
80
102
  File.write(@opf_file, doc.to_xml(indent: 2))
@@ -87,7 +109,7 @@ module EpubTools
87
109
  filenames.each do |filename|
88
110
  # Create a new <li><a href="...">Label</a></li> element
89
111
  label = File.basename(filename, '.xhtml').gsub('_', ' ').capitalize
90
- label = "Prologue" if label == "Chapter 0"
112
+ label = 'Prologue' if label == 'Chapter 0'
91
113
  li = Nokogiri::XML::Node.new('li', doc)
92
114
  a = Nokogiri::XML::Node.new('a', doc)
93
115
  a['href'] = filename
@@ -0,0 +1,47 @@
1
+ module EpubTools
2
+ module CLI
3
+ # Manages the registration and retrieval of commands
4
+ class CommandRegistry
5
+ attr_reader :commands
6
+
7
+ def initialize
8
+ @commands = {}
9
+ end
10
+
11
+ # Register a new command in the registry
12
+ # @param name [String] the command name
13
+ # @param command_class [Class] the class that implements the command
14
+ # @param required_keys [Array<Symbol>] keys that must be present in options
15
+ # @param default_options [Hash] default options for the command
16
+ # @return [self]
17
+ def register(name, command_class, required_keys = [], default_options = {})
18
+ @commands[name] = {
19
+ class: command_class,
20
+ required_keys: required_keys,
21
+ default_options: default_options
22
+ }
23
+ self
24
+ end
25
+
26
+ # Get a command by name
27
+ # @param name [String] the command name
28
+ # @return [Hash, nil] the command configuration or nil if not found
29
+ def get(name)
30
+ @commands[name]
31
+ end
32
+
33
+ # Get all available command names
34
+ # @return [Array<String>] list of registered command names
35
+ def available_commands
36
+ @commands.keys
37
+ end
38
+
39
+ # Check if a command is registered
40
+ # @param name [String] the command name
41
+ # @return [Boolean] true if command exists
42
+ def command_exists?(name)
43
+ @commands.key?(name)
44
+ end
45
+ end
46
+ end
47
+ end