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.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.rubocop.yml +41 -0
- data/.tool-versions +1 -0
- data/Gemfile +13 -9
- data/Gemfile.lock +63 -12
- data/README.md +26 -6
- data/bin/epub-tools +3 -109
- data/epub_tools.gemspec +6 -8
- data/lib/epub_tools/add_chapters.rb +41 -19
- data/lib/epub_tools/cli/command_registry.rb +47 -0
- data/lib/epub_tools/cli/option_builder.rb +164 -0
- data/lib/epub_tools/cli/runner.rb +164 -0
- data/lib/epub_tools/cli.rb +45 -0
- data/lib/epub_tools/compile_book.rb +60 -28
- data/lib/epub_tools/epub_initializer.rb +38 -28
- data/lib/epub_tools/loggable.rb +11 -0
- data/lib/epub_tools/pack_ebook.rb +18 -12
- data/lib/epub_tools/split_chapters.rb +31 -21
- data/lib/epub_tools/{text_style_class_finder.rb → style_finder.rb} +21 -17
- data/lib/epub_tools/unpack_ebook.rb +17 -12
- data/lib/epub_tools/version.rb +1 -1
- data/lib/epub_tools/xhtml_cleaner.rb +17 -13
- data/lib/epub_tools/xhtml_extractor.rb +20 -11
- data/lib/epub_tools.rb +2 -1
- data/test/add_chapters_test.rb +12 -5
- data/test/cli/command_registry_test.rb +66 -0
- data/test/cli/option_builder_test.rb +173 -0
- data/test/cli/runner_test.rb +91 -0
- data/test/cli_commands_test.rb +100 -0
- data/test/cli_test.rb +4 -0
- data/test/cli_version_test.rb +5 -3
- data/test/compile_book_test.rb +11 -2
- data/test/epub_initializer_test.rb +51 -31
- data/test/pack_ebook_test.rb +14 -8
- data/test/split_chapters_test.rb +22 -1
- data/test/{text_style_class_finder_test.rb → style_finder_test.rb} +7 -6
- data/test/test_helper.rb +4 -5
- data/test/unpack_ebook_test.rb +21 -5
- data/test/xhtml_cleaner_test.rb +13 -7
- data/test/xhtml_extractor_test.rb +17 -1
- metadata +21 -38
- data/.ruby-version +0 -1
- data/lib/epub_tools/cli_helper.rb +0 -31
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3920ca32a1d5595866904c273c561dceec0021c259708317938e6ee6825adb3f
|
4
|
+
data.tar.gz: '0490275853356243b7b9ba25367439e6cb2fbee44ac43c71984a9b91f06e2464'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1c0dfa04ad854e968f2e0ecb1ef3572f598269fedd91278602e0dd3bf524efc2358bab95b800966df175add2b9357ca4d0de366b4d21b30400d599946d299a72
|
7
|
+
data.tar.gz: df4bfb3cc82a0271ec320d523d41fcb8f36dd2b93d39419d5b719ea1bd24bf089163d80cd408dd6da16482e374567baf003e1e06e2eeee24cff844fcbbf06eaf
|
data/.gitignore
CHANGED
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
|
5
|
+
source 'https://rubygems.org'
|
6
6
|
|
7
|
-
gem
|
8
|
-
gem
|
9
|
-
gem
|
7
|
+
gem 'nokogiri', '~> 1.18'
|
8
|
+
gem 'rake', '~> 13.2'
|
9
|
+
gem 'rubyzip', '~> 2.4'
|
10
10
|
|
11
|
-
group :test do
|
12
|
-
gem
|
13
|
-
gem
|
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
|
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.
|
12
|
+
nokogiri (1.18.9-aarch64-linux-gnu)
|
7
13
|
racc (~> 1.4)
|
8
|
-
nokogiri (1.18.
|
14
|
+
nokogiri (1.18.9-aarch64-linux-musl)
|
9
15
|
racc (~> 1.4)
|
10
|
-
nokogiri (1.18.
|
16
|
+
nokogiri (1.18.9-arm-linux-gnu)
|
11
17
|
racc (~> 1.4)
|
12
|
-
nokogiri (1.18.
|
18
|
+
nokogiri (1.18.9-arm-linux-musl)
|
13
19
|
racc (~> 1.4)
|
14
|
-
nokogiri (1.18.
|
20
|
+
nokogiri (1.18.9-arm64-darwin)
|
15
21
|
racc (~> 1.4)
|
16
|
-
nokogiri (1.18.
|
22
|
+
nokogiri (1.18.9-x86_64-darwin)
|
17
23
|
racc (~> 1.4)
|
18
|
-
nokogiri (1.18.
|
24
|
+
nokogiri (1.18.9-x86_64-linux-gnu)
|
19
25
|
racc (~> 1.4)
|
20
|
-
nokogiri (1.18.
|
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
|
-
|
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.
|
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.
|
103
|
+
ruby 3.4.5p51
|
53
104
|
|
54
105
|
BUNDLED WITH
|
55
|
-
2.
|
106
|
+
2.7.0
|
data/README.md
CHANGED
@@ -1,16 +1,20 @@
|
|
1
1
|
# EPUB Tools
|
2
2
|
|
3
|
-
[](https://github.com/jaimerodas/epub_tools/actions) [](LICENSE)
|
3
|
+
[](https://github.com/jaimerodas/epub_tools/actions) [](LICENSE) [](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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
5
|
-
|
6
|
-
|
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 =
|
15
|
-
spec.metadata
|
16
|
-
|
17
|
-
|
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
|
-
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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 =
|
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
|