epub_tools 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.document +2 -0
- data/.github/workflows/ci.yml +9 -8
- data/.gitignore +4 -0
- data/.rubocop.yml +41 -0
- data/Gemfile +17 -8
- data/Gemfile.lock +51 -0
- data/LICENSE +21 -0
- data/README.md +21 -3
- data/bin/epub-tools +3 -109
- data/epub_tools.gemspec +6 -8
- data/lib/epub_tools/add_chapters.rb +124 -0
- 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 +77 -34
- data/lib/epub_tools/epub_initializer.rb +48 -26
- data/lib/epub_tools/loggable.rb +11 -0
- data/lib/epub_tools/pack_ebook.rb +20 -13
- data/lib/epub_tools/split_chapters.rb +40 -21
- data/lib/epub_tools/style_finder.rb +58 -0
- data/lib/epub_tools/unpack_ebook.rb +23 -16
- data/lib/epub_tools/version.rb +2 -1
- data/lib/epub_tools/xhtml_cleaner.rb +28 -8
- data/lib/epub_tools/xhtml_extractor.rb +23 -10
- data/lib/epub_tools.rb +4 -2
- data/test/{add_chapters_to_epub_test.rb → add_chapters_test.rb} +14 -7
- 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 +24 -39
- data/lib/epub_tools/add_chapters_to_epub.rb +0 -87
- data/lib/epub_tools/cli_helper.rb +0 -31
- data/lib/epub_tools/text_style_class_finder.rb +0 -47
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 67e06b8c37f922205445fb328c172184eb14436d876514e306892e17faf767e2
|
4
|
+
data.tar.gz: b0eb25344c96cca09889bc7854b40f48dca2bc49449583a1259dd7ac30803309
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b36393dbb0fcc6888a10a2bdb93d30fe88eb423034aacc7455bdb8338e4a0fccdfc8fac70b9998ccd66c1758956afcef0bd8ecd404aabe5eacfa4326b5f3b847
|
7
|
+
data.tar.gz: f9b2071dc6a832b178fa77c8982f43f645a9ebed4059cd023863cde8ed6220300df1155cfea39ee35455188b2fcd6672abf56f3682a5f46c606891e4e1bb0347
|
data/.document
ADDED
data/.github/workflows/ci.yml
CHANGED
@@ -2,20 +2,21 @@ name: CI
|
|
2
2
|
|
3
3
|
on:
|
4
4
|
push:
|
5
|
-
branches: [
|
5
|
+
branches: [ main ]
|
6
6
|
pull_request:
|
7
|
-
branches: [
|
7
|
+
branches: [ main ]
|
8
8
|
|
9
9
|
jobs:
|
10
10
|
test:
|
11
11
|
runs-on: ubuntu-latest
|
12
|
+
strategy:
|
13
|
+
matrix:
|
14
|
+
ruby: ['3.2', '3.3', '3.4']
|
12
15
|
steps:
|
13
16
|
- uses: actions/checkout@v3
|
14
17
|
- uses: ruby/setup-ruby@v1
|
15
18
|
with:
|
16
|
-
ruby-version:
|
17
|
-
|
18
|
-
-
|
19
|
-
|
20
|
-
- name: Run tests
|
21
|
-
run: bundle exec rake test
|
19
|
+
ruby-version: ${{ matrix.ruby }}
|
20
|
+
- run: gem install bundler
|
21
|
+
- run: bundle install --jobs 4 --retry 3
|
22
|
+
- run: bundle exec rake test
|
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/Gemfile
CHANGED
@@ -1,14 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
ruby '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
|
17
|
+
end
|
18
|
+
|
19
|
+
group :doc do
|
20
|
+
gem 'rdoc', '~> 6.13'
|
21
|
+
gem 'webrick', '~> 1.9'
|
22
|
+
gem 'yard', '~> 0.9.37'
|
14
23
|
end
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,12 @@
|
|
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
|
+
json (2.11.3)
|
8
|
+
language_server-protocol (3.17.0.4)
|
9
|
+
lint_roller (1.1.0)
|
5
10
|
minitest (5.25.5)
|
6
11
|
nokogiri (1.18.8-aarch64-linux-gnu)
|
7
12
|
racc (~> 1.4)
|
@@ -19,8 +24,42 @@ GEM
|
|
19
24
|
racc (~> 1.4)
|
20
25
|
nokogiri (1.18.8-x86_64-linux-musl)
|
21
26
|
racc (~> 1.4)
|
27
|
+
parallel (1.27.0)
|
28
|
+
parser (3.3.8.0)
|
29
|
+
ast (~> 2.4.1)
|
30
|
+
racc
|
31
|
+
prism (1.4.0)
|
32
|
+
psych (5.2.3)
|
33
|
+
date
|
34
|
+
stringio
|
22
35
|
racc (1.8.1)
|
36
|
+
rainbow (3.1.1)
|
23
37
|
rake (13.2.1)
|
38
|
+
rdoc (6.13.1)
|
39
|
+
psych (>= 4.0.0)
|
40
|
+
regexp_parser (2.10.0)
|
41
|
+
rubocop (1.75.4)
|
42
|
+
json (~> 2.3)
|
43
|
+
language_server-protocol (~> 3.17.0.2)
|
44
|
+
lint_roller (~> 1.1.0)
|
45
|
+
parallel (~> 1.10)
|
46
|
+
parser (>= 3.3.0.2)
|
47
|
+
rainbow (>= 2.2.2, < 4.0)
|
48
|
+
regexp_parser (>= 2.9.3, < 3.0)
|
49
|
+
rubocop-ast (>= 1.44.0, < 2.0)
|
50
|
+
ruby-progressbar (~> 1.7)
|
51
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
52
|
+
rubocop-ast (1.44.1)
|
53
|
+
parser (>= 3.3.7.2)
|
54
|
+
prism (~> 1.4)
|
55
|
+
rubocop-minitest (0.38.0)
|
56
|
+
lint_roller (~> 1.1)
|
57
|
+
rubocop (>= 1.75.0, < 2.0)
|
58
|
+
rubocop-ast (>= 1.38.0, < 2.0)
|
59
|
+
rubocop-rake (0.7.1)
|
60
|
+
lint_roller (~> 1.1)
|
61
|
+
rubocop (>= 1.72.1)
|
62
|
+
ruby-progressbar (1.13.0)
|
24
63
|
rubyzip (2.4.1)
|
25
64
|
simplecov (0.22.0)
|
26
65
|
docile (~> 1.1)
|
@@ -28,6 +67,12 @@ GEM
|
|
28
67
|
simplecov_json_formatter (~> 0.1)
|
29
68
|
simplecov-html (0.13.1)
|
30
69
|
simplecov_json_formatter (0.1.4)
|
70
|
+
stringio (3.1.7)
|
71
|
+
unicode-display_width (3.1.4)
|
72
|
+
unicode-emoji (~> 4.0, >= 4.0.4)
|
73
|
+
unicode-emoji (4.0.4)
|
74
|
+
webrick (1.9.1)
|
75
|
+
yard (0.9.37)
|
31
76
|
|
32
77
|
PLATFORMS
|
33
78
|
aarch64-linux-gnu
|
@@ -43,8 +88,14 @@ DEPENDENCIES
|
|
43
88
|
minitest (~> 5.25)
|
44
89
|
nokogiri (~> 1.18)
|
45
90
|
rake (~> 13.2)
|
91
|
+
rdoc (~> 6.13)
|
92
|
+
rubocop (~> 1.75)
|
93
|
+
rubocop-minitest (~> 0.38.0)
|
94
|
+
rubocop-rake (~> 0.7.1)
|
46
95
|
rubyzip (~> 2.4)
|
47
96
|
simplecov
|
97
|
+
webrick (~> 1.9)
|
98
|
+
yard (~> 0.9.37)
|
48
99
|
|
49
100
|
RUBY VERSION
|
50
101
|
ruby 3.4.3p32
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Jaime Rodas
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
9
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
10
|
+
so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
# EPUB Tools
|
2
2
|
|
3
|
-
|
3
|
+
[](https://github.com/jaimerodas/epub_tools/actions) [](LICENSE)
|
4
|
+
|
5
|
+
**TL;DR:** A Ruby gem and CLI for working with EPUB files: extract, split, initialize, add chapters, pack, and unpack EPUB books.
|
4
6
|
|
5
7
|
## Installation
|
6
8
|
Install the gem via RubyGems:
|
@@ -52,7 +54,7 @@ epub-tools pack -i epub_dir -o MyBook.epub
|
|
52
54
|
|
53
55
|
# Unpack EPUB
|
54
56
|
epub-tools unpack -i MyBook.epub -o unpacked_dir
|
55
|
-
|
57
|
+
|
56
58
|
# Full compile workflow: extract, split, initialize, add, and pack into one EPUB
|
57
59
|
epub-tools compile -t "My Book" -a "Author Name" -s source_epubs -c cover.jpg -o MyBook.epub
|
58
60
|
```
|
@@ -88,7 +90,7 @@ EpubTools::EpubInitializer.new(
|
|
88
90
|
).run
|
89
91
|
|
90
92
|
# Add chapters
|
91
|
-
EpubTools::
|
93
|
+
EpubTools::AddChapters.new('chapters', 'epub_dir/OEBPS').run
|
92
94
|
|
93
95
|
# Pack EPUB
|
94
96
|
EpubTools::PackEbook.new('epub_dir', 'MyBook.epub').run
|
@@ -113,6 +115,22 @@ Enable coverage reporting:
|
|
113
115
|
```bash
|
114
116
|
COVERAGE=true bundle exec rake test
|
115
117
|
```
|
118
|
+
## Documentation
|
119
|
+
|
120
|
+
Detailed API documentation can be generated using YARD. To view the docs locally, serve the documentation locally with YARD:
|
121
|
+
|
122
|
+
```bash
|
123
|
+
bundle exec yard server --reload
|
124
|
+
```
|
125
|
+
|
126
|
+
Then navigate to http://localhost:8808 in your browser.
|
127
|
+
|
128
|
+
To (re)generate the documentation, install the documentation dependencies and run:
|
129
|
+
|
130
|
+
```bash
|
131
|
+
bundle install --with doc
|
132
|
+
bundle exec yard doc
|
133
|
+
```
|
116
134
|
|
117
135
|
## Contributing
|
118
136
|
Pull requests welcome! Please open an issue for major changes.
|
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::AddChaptersToEpub.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
|
@@ -0,0 +1,124 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'nokogiri'
|
3
|
+
require 'fileutils'
|
4
|
+
require_relative 'loggable'
|
5
|
+
|
6
|
+
module EpubTools
|
7
|
+
# Moves new chapters into an unpacked EPUB
|
8
|
+
class AddChapters
|
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[:epub_dir] || './epub/OEBPS')
|
22
|
+
@opf_file = File.join(@epub_dir, 'package.opf')
|
23
|
+
@nav_file = File.join(@epub_dir, 'nav.xhtml')
|
24
|
+
@verbose = options[:verbose] || false
|
25
|
+
|
26
|
+
validate_directories!
|
27
|
+
end
|
28
|
+
|
29
|
+
# It works like this:
|
30
|
+
# - First, the *.xhtml files are moved from +chapters_dir+ over to +epub_dir+
|
31
|
+
# - Then, new entries will be added to the manifest and spine of the EPUB's +package.opf+ file.
|
32
|
+
# It will sort the files by extracting the chapter number.
|
33
|
+
# - Finally, it will update the +nav.xhtml+ file with the new chapters. Note that if there's a
|
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
|
36
|
+
def run
|
37
|
+
moved_files = move_chapters
|
38
|
+
update_package_opf(moved_files)
|
39
|
+
update_nav_xhtml(moved_files)
|
40
|
+
moved_files.each { |f| log("Moved: #{f}") }
|
41
|
+
moved_files
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
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
|
+
|
58
|
+
def move_chapters
|
59
|
+
# Sort by chapter number (numeric)
|
60
|
+
chapter_files = Dir.glob(File.join(@chapters_dir, '*.xhtml')).sort_by do |path|
|
61
|
+
# extract first integer from filename (e.g. chapter_10.xhtml -> 10)
|
62
|
+
File.basename(path)[/\d+/].to_i
|
63
|
+
end
|
64
|
+
|
65
|
+
raise ArgumentError, "No .xhtml files found in '#{@chapters_dir}'" if chapter_files.empty?
|
66
|
+
|
67
|
+
chapter_files.each do |file|
|
68
|
+
FileUtils.mv(file, @epub_dir)
|
69
|
+
end
|
70
|
+
chapter_files.map { |f| File.basename(f) }
|
71
|
+
end
|
72
|
+
|
73
|
+
def chapter_id(filename)
|
74
|
+
match = filename.match(/chapter_(\d+)\.xhtml/)
|
75
|
+
match ? "chap#{match[1]}" : File.basename(filename, '.xhtml')
|
76
|
+
end
|
77
|
+
|
78
|
+
def update_package_opf(filenames)
|
79
|
+
doc = Nokogiri::XML(File.read(@opf_file)) { |config| config.default_xml.noblanks }
|
80
|
+
manifest = doc.at_xpath('//xmlns:manifest')
|
81
|
+
spine = doc.at_xpath('//xmlns:spine')
|
82
|
+
|
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
|
101
|
+
|
102
|
+
File.write(@opf_file, doc.to_xml(indent: 2))
|
103
|
+
end
|
104
|
+
|
105
|
+
def update_nav_xhtml(filenames)
|
106
|
+
doc = Nokogiri::XML(File.read(@nav_file)) { |config| config.default_xml.noblanks }
|
107
|
+
nav = doc.at_xpath('//xmlns:nav[@epub:type="toc"]/xmlns:ol')
|
108
|
+
|
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
|
120
|
+
|
121
|
+
File.write(@nav_file, doc.to_xml(indent: 2))
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -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
|