epub_tools 0.6.0 → 0.9.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/.github/workflows/ci.yml +3 -8
- data/.gitignore +0 -1
- data/.tool-versions +1 -1
- data/CLAUDE.md +17 -25
- data/Gemfile +5 -8
- data/Gemfile.lock +60 -54
- data/README.md +31 -4
- data/bin/epub-tools +1 -3
- data/epub_tools.gemspec +3 -4
- data/lib/epub_tools/add_chapters.rb +17 -18
- data/lib/epub_tools/append_book.rb +8 -15
- data/lib/epub_tools/book_builder.rb +28 -22
- data/lib/epub_tools/chapter_order.rb +33 -0
- data/lib/epub_tools/chapter_validator.rb +6 -3
- data/lib/epub_tools/cli.rb +121 -23
- data/lib/epub_tools/compile_book.rb +1 -1
- data/lib/epub_tools/epub_initializer.rb +93 -99
- data/lib/epub_tools/pack_ebook.rb +2 -14
- data/lib/epub_tools/pdf_converter.rb +113 -0
- data/lib/epub_tools/pdf_layout.rb +116 -0
- data/lib/epub_tools/set_cover.rb +116 -0
- data/lib/epub_tools/split_chapters.rb +22 -20
- data/lib/epub_tools/style_finder.rb +11 -40
- data/lib/epub_tools/unpack_ebook.rb +2 -20
- data/lib/epub_tools/version.rb +1 -1
- data/lib/epub_tools/xhtml_cleaner.rb +4 -7
- data/lib/epub_tools/xhtml_extractor.rb +1 -7
- data/lib/epub_tools.rb +2 -1
- data/test/add_chapters_test.rb +64 -0
- data/test/append_book_test.rb +11 -11
- data/test/chapter_validator_test.rb +12 -0
- data/test/cli_test.rb +40 -3
- data/test/compile_book_test.rb +14 -0
- data/test/epub_initializer_test.rb +18 -0
- data/test/pdf_converter_test.rb +69 -0
- data/test/pdf_layout_test.rb +57 -0
- data/test/set_cover_test.rb +68 -0
- data/test/split_chapters_test.rb +18 -12
- data/test/style_finder_test.rb +3 -9
- data/test/test_helper.rb +1 -1
- data/test/unpack_ebook_test.rb +1 -1
- data/test/xhtml_cleaner_test.rb +1 -4
- metadata +14 -35
- data/lib/epub_tools/chapter_marker_detector.rb +0 -46
- data/lib/epub_tools/cli/command_options_configurator.rb +0 -128
- data/lib/epub_tools/cli/command_registry.rb +0 -49
- data/lib/epub_tools/cli/option_builder.rb +0 -166
- data/lib/epub_tools/cli/runner.rb +0 -114
- data/lib/epub_tools/compile_workspace.rb +0 -40
- data/lib/epub_tools/epub_configuration.rb +0 -33
- data/lib/epub_tools/epub_file_writer.rb +0 -57
- data/lib/epub_tools/epub_metadata_builder.rb +0 -92
- data/lib/epub_tools/xhtml_generator.rb +0 -71
- data/test/cli/command_registry_test.rb +0 -68
- data/test/cli/option_builder_test.rb +0 -183
- data/test/cli/runner_test.rb +0 -91
- data/test/compile_workspace_test.rb +0 -55
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 79e280c3b4dd2f362ee8c013e98f6ac44a9ea5c18535bd0b6dba67c1a5a49f52
|
|
4
|
+
data.tar.gz: 1c6a8eac67cced477f7312eb5ec50091048c5bf5932264b5de9f1396c8bd6879
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fe396cf56d97ee602ad326f50614f21edc31b0af9ef0ed82656826f567e5dc5ad7df92620e1ba6b79203dbabac75ee03cff2c2ec597cb8763b35ac601ee72a79
|
|
7
|
+
data.tar.gz: 6a293f1075e4ff4b2ee26a2e11c2aedc5ec39b56d638dacb0c8feae916eb405024be0006b7fa4310afa333b871429130c6ec2265f236b9b7e998591103066ba4
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -12,14 +12,9 @@ permissions:
|
|
|
12
12
|
jobs:
|
|
13
13
|
test:
|
|
14
14
|
runs-on: ubuntu-latest
|
|
15
|
-
strategy:
|
|
16
|
-
matrix:
|
|
17
|
-
ruby: ['3.2', '3.3', '3.4']
|
|
18
15
|
steps:
|
|
19
|
-
- uses: actions/checkout@
|
|
20
|
-
- uses: ruby/setup-ruby@v1
|
|
16
|
+
- uses: actions/checkout@v7
|
|
17
|
+
- uses: ruby/setup-ruby@v1 # Ruby version from .tool-versions
|
|
21
18
|
with:
|
|
22
|
-
|
|
23
|
-
- run: gem install bundler
|
|
24
|
-
- run: bundle install --jobs 4 --retry 3
|
|
19
|
+
bundler-cache: true
|
|
25
20
|
- run: bundle exec rake test
|
data/.gitignore
CHANGED
data/.tool-versions
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
ruby
|
|
1
|
+
ruby 4.0.9
|
data/CLAUDE.md
CHANGED
|
@@ -59,44 +59,38 @@ gem install ./epub_tools-*.gem
|
|
|
59
59
|
### Core Components
|
|
60
60
|
|
|
61
61
|
- **Main Module** (`lib/epub_tools.rb`): Entry point that requires all components
|
|
62
|
-
- **CLI
|
|
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
|
|
62
|
+
- **CLI** (`lib/epub_tools/cli.rb`): `CLI::COMMANDS` maps each command to its class, summary and flags
|
|
67
63
|
- **Core Classes**: Individual operation classes for EPUB manipulation
|
|
68
64
|
- `XHTMLExtractor`: Extracts XHTML files from EPUB archives
|
|
69
|
-
- `
|
|
70
|
-
- `
|
|
65
|
+
- `PDFConverter`: Converts chapter PDFs (named `12a Title.pdf`) into chapter XHTMLs via poppler's `pdftohtml -xml`
|
|
66
|
+
- `SplitChapters`: Splits XHTML files into separate chapters at Chapter N / Chapter N (continued) / Prologue markers
|
|
67
|
+
- `EpubInitializer`: Creates new EPUB directory structure
|
|
71
68
|
- `AddChapters`: Adds chapter files to existing EPUB
|
|
72
69
|
- `PackEbook`: Packages EPUB directories into .epub files
|
|
73
70
|
- `UnpackEbook`: Unpacks .epub files into directories
|
|
71
|
+
- `SetCover`: Adds or replaces a cover, in a packed EPUB (`cover` command) or an unpacked one (used by `EpubInitializer`)
|
|
74
72
|
- **Workflow Classes**: Orchestrators built on a shared base class
|
|
75
|
-
- `BookBuilder`: Base class with template method pattern (extract → split → validate → add → pack)
|
|
73
|
+
- `BookBuilder`: Base class with template method pattern (extract → split → convert PDFs → validate → add → pack)
|
|
76
74
|
- `CompileBook`: Creates a new EPUB from source EPUBs (inherits BookBuilder)
|
|
77
75
|
- `AppendBook`: Appends chapters from source EPUBs to an existing EPUB (inherits BookBuilder)
|
|
78
|
-
- **Supporting Classes
|
|
79
|
-
- `CompileWorkspace`: Manages build directories for book-building workflows
|
|
76
|
+
- **Supporting Classes**
|
|
80
77
|
- `ChapterValidator`: Validates chapter sequence completeness
|
|
81
|
-
- `
|
|
82
|
-
- `
|
|
83
|
-
- `
|
|
84
|
-
- `EpubMetadataBuilder`: Builds OPF metadata content
|
|
85
|
-
- `EpubFileWriter`: Handles EPUB file writing operations
|
|
78
|
+
- `PDFLayout`: Rebuilds paragraphs, italics, scene breaks and lists from `pdftohtml -xml` line positions
|
|
79
|
+
- `ChapterOrder`: Reading order of chapter files (1, 1_5, 1a, 1b, 2); inserts added chapters in place
|
|
80
|
+
- `StyleFinder` / `XHTMLCleaner`: Find Google Docs' bold/italic classes and clean split chapters with them
|
|
86
81
|
|
|
87
82
|
### CLI Architecture
|
|
88
83
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
3. The `CommandOptionsConfigurator` handles command-specific option setup
|
|
84
|
+
1. Each command is an entry in `CLI::COMMANDS`: its class, usage summary, and flags as `[short, long, description, option key]`
|
|
85
|
+
2. Flags whose description ends in `(required)` are enforced; commands get `-q/--quiet` unless marked `verbose: false`
|
|
86
|
+
3. `CLI.run` parses the flags and calls `command_class.new(options).run`
|
|
93
87
|
4. Each command class implements a `run` method and uses the `Loggable` mixin for verbose output
|
|
94
88
|
|
|
95
89
|
### Dependencies
|
|
96
90
|
|
|
97
91
|
- **nokogiri**: XML/HTML parsing for EPUB content
|
|
98
92
|
- **rubyzip**: ZIP file manipulation for EPUB packaging
|
|
99
|
-
- **rake**: Build tasks and testing
|
|
93
|
+
- **rake**: Build tasks and testing (development only)
|
|
100
94
|
- **minitest**: Testing framework
|
|
101
95
|
- **rubocop**: Code linting with custom configuration
|
|
102
96
|
- **simplecov**: Test coverage reporting
|
|
@@ -115,14 +109,12 @@ The CLI uses a registry-based system where:
|
|
|
115
109
|
Tests use Minitest with:
|
|
116
110
|
- `test_helper.rb` sets up SimpleCov coverage
|
|
117
111
|
- Tests in `test/` directory follow `*_test.rb` naming
|
|
118
|
-
- CLI tests verify
|
|
112
|
+
- CLI tests verify option parsing and dispatch; `cli_commands_test.rb` runs the real executable
|
|
119
113
|
- Individual component tests verify core functionality
|
|
120
114
|
|
|
121
115
|
### Code Quality
|
|
122
116
|
|
|
123
|
-
|
|
124
|
-
-
|
|
125
|
-
- **Open/Closed**: Extensible design through composition and dependency injection
|
|
126
|
-
- **Dependency Inversion**: Configuration objects and factory patterns
|
|
117
|
+
- One class per operation, each with an options hash and a `run` method
|
|
118
|
+
- Prefer plain methods, stdlib and the existing dependencies over new helper classes
|
|
127
119
|
|
|
128
120
|
RuboCop configuration excludes test files from metrics cops while maintaining strict standards for production code.
|
data/Gemfile
CHANGED
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
ruby '>= 3.2'
|
|
4
|
-
|
|
5
3
|
source 'https://rubygems.org'
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
gem 'rake', '~> 13.2'
|
|
9
|
-
gem 'rubyzip', '~> 3.2'
|
|
5
|
+
gemspec
|
|
10
6
|
|
|
11
7
|
group :test, :development do
|
|
12
8
|
gem 'minitest', '~> 6.0'
|
|
9
|
+
gem 'rake', '~> 13.2'
|
|
13
10
|
gem 'rubocop', '~> 1.75', require: false
|
|
14
|
-
gem 'rubocop-minitest', '~> 0.
|
|
11
|
+
gem 'rubocop-minitest', '~> 0.41.0', require: false
|
|
15
12
|
gem 'rubocop-rake', '~> 0.7.1', require: false
|
|
16
13
|
gem 'simplecov', require: false
|
|
17
14
|
end
|
|
18
15
|
|
|
19
16
|
group :doc do
|
|
20
|
-
gem 'rdoc', '~>
|
|
17
|
+
gem 'rdoc', '~> 8.1'
|
|
21
18
|
gem 'webrick', '~> 1.9'
|
|
22
|
-
gem 'yard', '~> 0.9.
|
|
19
|
+
gem 'yard', '~> 0.9.44'
|
|
23
20
|
end
|
data/Gemfile.lock
CHANGED
|
@@ -1,85 +1,95 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
epub_tools (0.9.0)
|
|
5
|
+
nokogiri (~> 1.19)
|
|
6
|
+
rubyzip (~> 3.7)
|
|
7
|
+
|
|
1
8
|
GEM
|
|
2
9
|
remote: https://rubygems.org/
|
|
3
10
|
specs:
|
|
4
11
|
ast (2.4.3)
|
|
5
|
-
date (3.5.1)
|
|
6
|
-
docile (1.4.1)
|
|
7
12
|
drb (2.2.3)
|
|
8
|
-
erb (6.0.
|
|
9
|
-
json (
|
|
10
|
-
language_server-protocol (3.17.0.
|
|
13
|
+
erb (6.0.7)
|
|
14
|
+
json (3.1.0)
|
|
15
|
+
language_server-protocol (3.17.0.6)
|
|
11
16
|
lint_roller (1.1.0)
|
|
12
|
-
|
|
17
|
+
logger (1.7.0)
|
|
18
|
+
minitest (6.1.0)
|
|
13
19
|
drb (~> 2.0)
|
|
14
|
-
prism (
|
|
15
|
-
nokogiri (1.19.
|
|
20
|
+
prism (>= 1.5, < 3)
|
|
21
|
+
nokogiri (1.19.7-aarch64-linux-gnu)
|
|
16
22
|
racc (~> 1.4)
|
|
17
|
-
nokogiri (1.19.
|
|
23
|
+
nokogiri (1.19.7-aarch64-linux-musl)
|
|
18
24
|
racc (~> 1.4)
|
|
19
|
-
nokogiri (1.19.
|
|
25
|
+
nokogiri (1.19.7-arm-linux-gnu)
|
|
20
26
|
racc (~> 1.4)
|
|
21
|
-
nokogiri (1.19.
|
|
27
|
+
nokogiri (1.19.7-arm-linux-musl)
|
|
22
28
|
racc (~> 1.4)
|
|
23
|
-
nokogiri (1.19.
|
|
29
|
+
nokogiri (1.19.7-arm64-darwin)
|
|
24
30
|
racc (~> 1.4)
|
|
25
|
-
nokogiri (1.19.
|
|
31
|
+
nokogiri (1.19.7-x86_64-darwin)
|
|
26
32
|
racc (~> 1.4)
|
|
27
|
-
nokogiri (1.19.
|
|
33
|
+
nokogiri (1.19.7-x86_64-linux-gnu)
|
|
28
34
|
racc (~> 1.4)
|
|
29
|
-
nokogiri (1.19.
|
|
35
|
+
nokogiri (1.19.7-x86_64-linux-musl)
|
|
30
36
|
racc (~> 1.4)
|
|
31
|
-
parallel (
|
|
32
|
-
parser (3.3.
|
|
37
|
+
parallel (2.3.0)
|
|
38
|
+
parser (3.3.13.0)
|
|
33
39
|
ast (~> 2.4.1)
|
|
34
40
|
racc
|
|
35
|
-
prism (1.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
prism (2.1.0)
|
|
42
|
+
prism (2.1.0-aarch64-linux-gnu)
|
|
43
|
+
prism (2.1.0-aarch64-linux-musl)
|
|
44
|
+
prism (2.1.0-arm-linux-gnu)
|
|
45
|
+
prism (2.1.0-arm-linux-musl)
|
|
46
|
+
prism (2.1.0-arm64-darwin)
|
|
47
|
+
prism (2.1.0-x86_64-darwin)
|
|
48
|
+
prism (2.1.0-x86_64-linux-gnu)
|
|
49
|
+
prism (2.1.0-x86_64-linux-musl)
|
|
39
50
|
racc (1.8.1)
|
|
40
51
|
rainbow (3.1.1)
|
|
41
|
-
rake (13.
|
|
42
|
-
|
|
52
|
+
rake (13.4.2)
|
|
53
|
+
rbs (4.2.0)
|
|
54
|
+
logger
|
|
55
|
+
prism (>= 1.6.0)
|
|
56
|
+
tsort
|
|
57
|
+
rdoc (8.1.0)
|
|
43
58
|
erb
|
|
44
|
-
|
|
59
|
+
prism (>= 1.6.0)
|
|
60
|
+
rbs (>= 4.0.0)
|
|
45
61
|
tsort
|
|
46
|
-
regexp_parser (2.
|
|
47
|
-
rubocop (1.
|
|
48
|
-
json (
|
|
62
|
+
regexp_parser (2.13.1)
|
|
63
|
+
rubocop (1.92.2)
|
|
64
|
+
json (>= 2.3)
|
|
49
65
|
language_server-protocol (~> 3.17.0.2)
|
|
50
66
|
lint_roller (~> 1.1.0)
|
|
51
|
-
parallel (
|
|
67
|
+
parallel (>= 1.10)
|
|
52
68
|
parser (>= 3.3.0.2)
|
|
53
69
|
rainbow (>= 2.2.2, < 4.0)
|
|
54
|
-
regexp_parser (>= 2.
|
|
70
|
+
regexp_parser (>= 2.11.3, < 3.0)
|
|
55
71
|
rubocop-ast (>= 1.49.0, < 2.0)
|
|
56
72
|
ruby-progressbar (~> 1.7)
|
|
57
73
|
unicode-display_width (>= 2.4.0, < 4.0)
|
|
58
|
-
rubocop-ast (1.
|
|
74
|
+
rubocop-ast (1.51.0)
|
|
59
75
|
parser (>= 3.3.7.2)
|
|
60
|
-
prism (
|
|
61
|
-
rubocop-minitest (0.
|
|
76
|
+
prism (>= 1.7)
|
|
77
|
+
rubocop-minitest (0.41.0)
|
|
62
78
|
lint_roller (~> 1.1)
|
|
63
|
-
rubocop (>= 1.
|
|
79
|
+
rubocop (>= 1.89.0, < 2.0)
|
|
64
80
|
rubocop-ast (>= 1.38.0, < 2.0)
|
|
65
81
|
rubocop-rake (0.7.1)
|
|
66
82
|
lint_roller (~> 1.1)
|
|
67
83
|
rubocop (>= 1.72.1)
|
|
68
84
|
ruby-progressbar (1.13.0)
|
|
69
|
-
rubyzip (3.
|
|
70
|
-
simplecov (
|
|
71
|
-
docile (~> 1.1)
|
|
72
|
-
simplecov-html (~> 0.11)
|
|
73
|
-
simplecov_json_formatter (~> 0.1)
|
|
74
|
-
simplecov-html (0.13.2)
|
|
75
|
-
simplecov_json_formatter (0.1.4)
|
|
76
|
-
stringio (3.2.0)
|
|
85
|
+
rubyzip (3.7.0)
|
|
86
|
+
simplecov (1.3.2)
|
|
77
87
|
tsort (0.2.0)
|
|
78
|
-
unicode-display_width (3.
|
|
79
|
-
unicode-emoji (~> 4.
|
|
80
|
-
unicode-emoji (4.
|
|
88
|
+
unicode-display_width (3.3.0)
|
|
89
|
+
unicode-emoji (~> 4.3)
|
|
90
|
+
unicode-emoji (4.3.0)
|
|
81
91
|
webrick (1.9.2)
|
|
82
|
-
yard (0.9.
|
|
92
|
+
yard (0.9.45)
|
|
83
93
|
|
|
84
94
|
PLATFORMS
|
|
85
95
|
aarch64-linux-gnu
|
|
@@ -92,20 +102,16 @@ PLATFORMS
|
|
|
92
102
|
x86_64-linux-musl
|
|
93
103
|
|
|
94
104
|
DEPENDENCIES
|
|
105
|
+
epub_tools!
|
|
95
106
|
minitest (~> 6.0)
|
|
96
|
-
nokogiri (~> 1.18)
|
|
97
107
|
rake (~> 13.2)
|
|
98
|
-
rdoc (~>
|
|
108
|
+
rdoc (~> 8.1)
|
|
99
109
|
rubocop (~> 1.75)
|
|
100
|
-
rubocop-minitest (~> 0.
|
|
110
|
+
rubocop-minitest (~> 0.41.0)
|
|
101
111
|
rubocop-rake (~> 0.7.1)
|
|
102
|
-
rubyzip (~> 3.2)
|
|
103
112
|
simplecov
|
|
104
113
|
webrick (~> 1.9)
|
|
105
|
-
yard (~> 0.9.
|
|
106
|
-
|
|
107
|
-
RUBY VERSION
|
|
108
|
-
ruby 3.4.5p51
|
|
114
|
+
yard (~> 0.9.44)
|
|
109
115
|
|
|
110
116
|
BUNDLED WITH
|
|
111
|
-
|
|
117
|
+
4.0.24
|
data/README.md
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
9
9
|
### Requirements
|
|
10
|
-
- Ruby
|
|
10
|
+
- Ruby 4.0 or higher
|
|
11
|
+
- [poppler](https://poppler.freedesktop.org/) (`brew install poppler`), only for PDF sources
|
|
11
12
|
|
|
12
13
|
### Install from RubyGems
|
|
13
14
|
```bash
|
|
@@ -32,11 +33,13 @@ Commands:
|
|
|
32
33
|
- `init` Initialize a new EPUB directory structure
|
|
33
34
|
- `extract` Extract XHTML files from EPUB archives
|
|
34
35
|
- `split` Split an XHTML file into separate chapter files
|
|
36
|
+
- `pdf` Convert chapter PDFs into chapter XHTML files
|
|
35
37
|
- `add` Add chapter XHTML files into an existing EPUB
|
|
36
38
|
- `pack` Package an EPUB directory into a `.epub` file
|
|
37
39
|
- `unpack` Unpack a `.epub` file into a directory
|
|
38
|
-
- `
|
|
39
|
-
- `
|
|
40
|
+
- `cover` Add or replace the cover of a `.epub` file
|
|
41
|
+
- `compile` Takes EPUBs and/or chapter PDFs in a dir and compiles them into a single EPUB
|
|
42
|
+
- `append` Takes EPUBs and/or chapter PDFs in a dir and appends them to an existing EPUB
|
|
40
43
|
|
|
41
44
|
Run `epub-tools COMMAND --help` for details on options.
|
|
42
45
|
|
|
@@ -60,6 +63,9 @@ epub-tools pack -i epub_dir -o MyBook.epub
|
|
|
60
63
|
# Unpack EPUB
|
|
61
64
|
epub-tools unpack -i MyBook.epub -o unpacked_dir
|
|
62
65
|
|
|
66
|
+
# Add or replace the cover of an existing EPUB (keeps a MyBook.epub.bak backup)
|
|
67
|
+
epub-tools cover -i MyBook.epub -c cover.jpg
|
|
68
|
+
|
|
63
69
|
# Full compile workflow: extract, split, initialize, add, and pack into one EPUB
|
|
64
70
|
epub-tools compile -t "My Book" -a "Author Name" -s source_epubs -c cover.jpg -o MyBook.epub
|
|
65
71
|
|
|
@@ -67,6 +73,27 @@ epub-tools compile -t "My Book" -a "Author Name" -s source_epubs -c cover.jpg -o
|
|
|
67
73
|
epub-tools append -s new_epubs -t MyBook.epub
|
|
68
74
|
```
|
|
69
75
|
|
|
76
|
+
### Chapter PDFs
|
|
77
|
+
`compile` and `append` also take a directory of chapter PDFs, one chapter per file, named
|
|
78
|
+
`<number><optional letters> <Title>.pdf`:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
0 Dramatis Personae.pdf -> "Dramatis Personae" (chapter 0 is front matter)
|
|
82
|
+
1 First Day.pdf -> "Chapter 1: First Day"
|
|
83
|
+
1a An Interlude.pdf -> "Chapter 1a: An Interlude", between 1 and 2
|
|
84
|
+
Epilogue One Month.pdf -> "Epilogue: One Month", after every chapter
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Paragraphs, italics, bold and scene breaks (`***`) are rebuilt from the PDF layout, and the heading typed
|
|
88
|
+
inside the PDF is replaced by the one from the filename. To add new chapters later, put only the new PDFs
|
|
89
|
+
in a directory and `append` them; chapters are slotted into reading order, even a side chapter like `12a`
|
|
90
|
+
added after chapter 13 is already in the book. Chapters that are already in the book are refused.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
epub-tools compile -t "My Book" -a "Author Name" -s chapter_pdfs -o MyBook.epub
|
|
94
|
+
epub-tools append -s new_chapter_pdfs -t MyBook.epub
|
|
95
|
+
```
|
|
96
|
+
|
|
70
97
|
## Library Usage
|
|
71
98
|
Use the library directly in Ruby:
|
|
72
99
|
```ruby
|
|
@@ -95,7 +122,7 @@ EpubTools::XHTMLExtractor.new(
|
|
|
95
122
|
# Split a multi-chapter XHTML into individual chapter files
|
|
96
123
|
EpubTools::SplitChapters.new(
|
|
97
124
|
input_file: 'xhtml_output/chapter1.xhtml', book_title: 'My Book',
|
|
98
|
-
output_dir: 'chapters'
|
|
125
|
+
output_dir: 'chapters'
|
|
99
126
|
).run
|
|
100
127
|
|
|
101
128
|
# Initialize a new EPUB directory structure
|
data/bin/epub-tools
CHANGED
data/epub_tools.gemspec
CHANGED
|
@@ -13,14 +13,13 @@ Gem::Specification.new do |spec|
|
|
|
13
13
|
spec.files = `git ls-files`.split("\n")
|
|
14
14
|
spec.require_paths = ['lib']
|
|
15
15
|
spec.executables = ['epub-tools']
|
|
16
|
-
spec.required_ruby_version = '>=
|
|
16
|
+
spec.required_ruby_version = '>= 4.0'
|
|
17
17
|
spec.metadata = {
|
|
18
18
|
'source_code_uri' => 'https://github.com/jaimerodas/epub_tools/tree/main',
|
|
19
19
|
'homepage_uri' => 'https://github.com/jaimerodas/epub_tools',
|
|
20
20
|
'rubygems_mfa_required' => 'true'
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
spec.add_dependency 'nokogiri', '~> 1.
|
|
24
|
-
spec.add_dependency '
|
|
25
|
-
spec.add_dependency 'rubyzip', '~> 3.2'
|
|
23
|
+
spec.add_dependency 'nokogiri', '~> 1.19'
|
|
24
|
+
spec.add_dependency 'rubyzip', '~> 3.7'
|
|
26
25
|
end
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
1
|
# frozen_string_literal: true
|
|
3
2
|
|
|
4
3
|
require 'nokogiri'
|
|
5
4
|
require 'fileutils'
|
|
6
5
|
require_relative 'loggable'
|
|
6
|
+
require_relative 'chapter_order'
|
|
7
7
|
|
|
8
8
|
module EpubTools
|
|
9
9
|
# Moves new chapters into an unpacked EPUB
|
|
@@ -32,9 +32,10 @@ module EpubTools
|
|
|
32
32
|
# It works like this:
|
|
33
33
|
# - First, the *.xhtml files are moved from +chapters_dir+ over to +epub_dir+
|
|
34
34
|
# - Then, new entries will be added to the manifest and spine of the EPUB's +package.opf+ file.
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
35
|
+
# Chapters are placed in reading order by their number (+chapter_12a+ goes between 12 and 13),
|
|
36
|
+
# even when the book already contains later chapters.
|
|
37
|
+
# - Finally, it will update the +nav.xhtml+ file with the new chapters, labelled with each chapter's
|
|
38
|
+
# <tt><h1></tt>. Without one, the label comes from the filename (+chapter_0.xhtml+ is the Prologue).
|
|
38
39
|
# @return [Array<String>] List of moved chapter filenames
|
|
39
40
|
def run
|
|
40
41
|
moved_files = move_chapters
|
|
@@ -60,7 +61,7 @@ module EpubTools
|
|
|
60
61
|
|
|
61
62
|
def move_chapters
|
|
62
63
|
chapter_files = Dir.glob(File.join(@chapters_dir, '*.xhtml')).sort_by do |path|
|
|
63
|
-
|
|
64
|
+
ChapterOrder.sort_key(path)
|
|
64
65
|
end
|
|
65
66
|
|
|
66
67
|
raise ArgumentError, "No .xhtml files found in '#{@chapters_dir}'" if chapter_files.empty?
|
|
@@ -71,15 +72,6 @@ module EpubTools
|
|
|
71
72
|
chapter_files.map { |f| File.basename(f) }
|
|
72
73
|
end
|
|
73
74
|
|
|
74
|
-
def chapter_sort_key(filename)
|
|
75
|
-
basename = File.basename(filename, '.xhtml')
|
|
76
|
-
if (m = basename.match(/_(\d+)_5\z/))
|
|
77
|
-
m[1].to_f + 0.5
|
|
78
|
-
else
|
|
79
|
-
basename[/\d+/].to_f
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
75
|
def chapter_id(filename)
|
|
84
76
|
match = filename.match(/chapter_(\d+(?:_5)?)\.xhtml/)
|
|
85
77
|
match ? "chap#{match[1]}" : File.basename(filename, '.xhtml')
|
|
@@ -99,7 +91,9 @@ module EpubTools
|
|
|
99
91
|
doc = Nokogiri::XML(File.read(@nav_file)) { |config| config.default_xml.noblanks }
|
|
100
92
|
nav = doc.at_xpath('//xmlns:nav[@epub:type="toc"]/xmlns:ol')
|
|
101
93
|
|
|
102
|
-
filenames.each
|
|
94
|
+
filenames.each do |filename|
|
|
95
|
+
ChapterOrder.insert(nav, create_nav_link(doc, filename), filename) { |li| li.at_xpath('xmlns:a')&.[]('href') }
|
|
96
|
+
end
|
|
103
97
|
|
|
104
98
|
File.write(@nav_file, doc.to_xml(indent: 2))
|
|
105
99
|
end
|
|
@@ -113,7 +107,11 @@ module EpubTools
|
|
|
113
107
|
li
|
|
114
108
|
end
|
|
115
109
|
|
|
110
|
+
# Uses the chapter's own <h1> when it has one, falling back to a label derived from the filename
|
|
116
111
|
def format_chapter_label(filename)
|
|
112
|
+
heading = Nokogiri::XML(File.read(File.join(@epub_dir, filename))).at_xpath('//*[local-name()="h1"]')
|
|
113
|
+
return heading.text.strip unless heading.nil? || heading.text.strip.empty?
|
|
114
|
+
|
|
117
115
|
basename = File.basename(filename, '.xhtml')
|
|
118
116
|
return 'Prologue' if basename == 'chapter_0'
|
|
119
117
|
|
|
@@ -127,7 +125,7 @@ module EpubTools
|
|
|
127
125
|
def update_opf_for_file(doc, manifest, spine, filename)
|
|
128
126
|
id = chapter_id(filename)
|
|
129
127
|
add_manifest_item(doc, manifest, filename, id) unless manifest_item_exists?(doc, filename)
|
|
130
|
-
add_spine_itemref(doc, spine, id) unless spine_itemref_exists?(doc, id)
|
|
128
|
+
add_spine_itemref(doc, spine, id, filename) unless spine_itemref_exists?(doc, id)
|
|
131
129
|
end
|
|
132
130
|
|
|
133
131
|
def manifest_item_exists?(doc, filename)
|
|
@@ -146,10 +144,11 @@ module EpubTools
|
|
|
146
144
|
manifest.add_child(item)
|
|
147
145
|
end
|
|
148
146
|
|
|
149
|
-
def add_spine_itemref(doc, spine, id)
|
|
147
|
+
def add_spine_itemref(doc, spine, id, filename)
|
|
150
148
|
itemref = Nokogiri::XML::Node.new('itemref', doc)
|
|
151
149
|
itemref['idref'] = id
|
|
152
|
-
|
|
150
|
+
hrefs = doc.xpath('//xmlns:manifest/xmlns:item').to_h { |item| [item['id'], item['href']] }
|
|
151
|
+
ChapterOrder.insert(spine, itemref, filename) { |ref| hrefs[ref['idref']] }
|
|
153
152
|
end
|
|
154
153
|
end
|
|
155
154
|
end
|
|
@@ -33,7 +33,7 @@ module EpubTools
|
|
|
33
33
|
|
|
34
34
|
def finalize_and_cleanup
|
|
35
35
|
log "Done. Updated EPUB: #{@target_epub} (backup: #{@backup_path})"
|
|
36
|
-
|
|
36
|
+
FileUtils.rm_rf(build_dir)
|
|
37
37
|
@target_epub
|
|
38
38
|
end
|
|
39
39
|
|
|
@@ -45,7 +45,7 @@ module EpubTools
|
|
|
45
45
|
|
|
46
46
|
def unpack_target
|
|
47
47
|
log 'Unpacking target EPUB...'
|
|
48
|
-
UnpackEbook.new(epub_file: @target_epub, output_dir:
|
|
48
|
+
UnpackEbook.new(epub_file: @target_epub, output_dir: epub_dir, verbose: verbose).run
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def read_target_title
|
|
@@ -56,25 +56,18 @@ module EpubTools
|
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
def detect_conflicts
|
|
59
|
-
|
|
60
|
-
existing_numbers = chapter_numbers_in(epub_oebps_dir)
|
|
61
|
-
conflicts = new_numbers & existing_numbers
|
|
59
|
+
conflicts = chapter_numbers_in(chapters_dir) & chapter_numbers_in(epub_oebps_dir)
|
|
62
60
|
return if conflicts.empty?
|
|
63
61
|
|
|
64
|
-
formatted = conflicts.sort.map { |n| n == n.to_i ? n.to_i.to_s : n.to_s }
|
|
65
62
|
raise ArgumentError,
|
|
66
|
-
"Chapter number conflict: chapters #{
|
|
67
|
-
'Renumber the source chapters or remove conflicting chapters from the target.'
|
|
63
|
+
"Chapter number conflict: chapters #{conflicts.sort_by(&:to_f).join(', ')} already exist in the " \
|
|
64
|
+
'target EPUB. Renumber the source chapters or remove conflicting chapters from the target.'
|
|
68
65
|
end
|
|
69
66
|
|
|
67
|
+
# @return [Array<String>] Chapter numbers as written in filenames: chapter_3_5 is "3.5", chapter_3a is "3a"
|
|
70
68
|
def chapter_numbers_in(dir)
|
|
71
|
-
Dir.glob(File.join(dir, 'chapter_*.xhtml')).
|
|
72
|
-
|
|
73
|
-
if (m = basename.match(/_(\d+)_5\z/))
|
|
74
|
-
m[1].to_f + 0.5
|
|
75
|
-
elsif (m = basename.match(/_(\d+)\z/))
|
|
76
|
-
m[1].to_f
|
|
77
|
-
end
|
|
69
|
+
Dir.glob(File.join(dir, 'chapter_*.xhtml')).map do |path|
|
|
70
|
+
File.basename(path, '.xhtml').delete_prefix('chapter_').tr('_', '.')
|
|
78
71
|
end
|
|
79
72
|
end
|
|
80
73
|
end
|