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