epub_tools 0.4.1 → 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/.rubocop.yml +10 -17
- data/CLAUDE.md +124 -0
- data/Gemfile +4 -4
- data/Gemfile.lock +39 -34
- data/Rakefile +2 -0
- data/bin/epub-tools +2 -0
- data/epub_tools.gemspec +3 -1
- data/lib/epub_tools/add_chapters.rb +47 -29
- 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 +49 -25
- 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 +2 -0
- 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 +13 -3
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/.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/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,45 +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)
|
|
9
10
|
language_server-protocol (3.17.0.5)
|
|
10
11
|
lint_roller (1.1.0)
|
|
11
|
-
minitest (
|
|
12
|
-
|
|
12
|
+
minitest (6.0.2)
|
|
13
|
+
drb (~> 2.0)
|
|
14
|
+
prism (~> 1.5)
|
|
15
|
+
nokogiri (1.19.1-aarch64-linux-gnu)
|
|
13
16
|
racc (~> 1.4)
|
|
14
|
-
nokogiri (1.
|
|
17
|
+
nokogiri (1.19.1-aarch64-linux-musl)
|
|
15
18
|
racc (~> 1.4)
|
|
16
|
-
nokogiri (1.
|
|
19
|
+
nokogiri (1.19.1-arm-linux-gnu)
|
|
17
20
|
racc (~> 1.4)
|
|
18
|
-
nokogiri (1.
|
|
21
|
+
nokogiri (1.19.1-arm-linux-musl)
|
|
19
22
|
racc (~> 1.4)
|
|
20
|
-
nokogiri (1.
|
|
23
|
+
nokogiri (1.19.1-arm64-darwin)
|
|
21
24
|
racc (~> 1.4)
|
|
22
|
-
nokogiri (1.
|
|
25
|
+
nokogiri (1.19.1-x86_64-darwin)
|
|
23
26
|
racc (~> 1.4)
|
|
24
|
-
nokogiri (1.
|
|
27
|
+
nokogiri (1.19.1-x86_64-linux-gnu)
|
|
25
28
|
racc (~> 1.4)
|
|
26
|
-
nokogiri (1.
|
|
29
|
+
nokogiri (1.19.1-x86_64-linux-musl)
|
|
27
30
|
racc (~> 1.4)
|
|
28
31
|
parallel (1.27.0)
|
|
29
|
-
parser (3.3.
|
|
32
|
+
parser (3.3.10.2)
|
|
30
33
|
ast (~> 2.4.1)
|
|
31
34
|
racc
|
|
32
|
-
prism (1.
|
|
33
|
-
psych (5.
|
|
35
|
+
prism (1.9.0)
|
|
36
|
+
psych (5.3.1)
|
|
34
37
|
date
|
|
35
38
|
stringio
|
|
36
39
|
racc (1.8.1)
|
|
37
40
|
rainbow (3.1.1)
|
|
38
|
-
rake (13.3.
|
|
39
|
-
rdoc (
|
|
41
|
+
rake (13.3.1)
|
|
42
|
+
rdoc (7.2.0)
|
|
40
43
|
erb
|
|
41
44
|
psych (>= 4.0.0)
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
tsort
|
|
46
|
+
regexp_parser (2.11.3)
|
|
47
|
+
rubocop (1.84.2)
|
|
44
48
|
json (~> 2.3)
|
|
45
49
|
language_server-protocol (~> 3.17.0.2)
|
|
46
50
|
lint_roller (~> 1.1.0)
|
|
@@ -48,13 +52,13 @@ GEM
|
|
|
48
52
|
parser (>= 3.3.0.2)
|
|
49
53
|
rainbow (>= 2.2.2, < 4.0)
|
|
50
54
|
regexp_parser (>= 2.9.3, < 3.0)
|
|
51
|
-
rubocop-ast (>= 1.
|
|
55
|
+
rubocop-ast (>= 1.49.0, < 2.0)
|
|
52
56
|
ruby-progressbar (~> 1.7)
|
|
53
57
|
unicode-display_width (>= 2.4.0, < 4.0)
|
|
54
|
-
rubocop-ast (1.
|
|
58
|
+
rubocop-ast (1.49.0)
|
|
55
59
|
parser (>= 3.3.7.2)
|
|
56
|
-
prism (~> 1.
|
|
57
|
-
rubocop-minitest (0.
|
|
60
|
+
prism (~> 1.7)
|
|
61
|
+
rubocop-minitest (0.39.1)
|
|
58
62
|
lint_roller (~> 1.1)
|
|
59
63
|
rubocop (>= 1.75.0, < 2.0)
|
|
60
64
|
rubocop-ast (>= 1.38.0, < 2.0)
|
|
@@ -62,19 +66,20 @@ GEM
|
|
|
62
66
|
lint_roller (~> 1.1)
|
|
63
67
|
rubocop (>= 1.72.1)
|
|
64
68
|
ruby-progressbar (1.13.0)
|
|
65
|
-
rubyzip (2.
|
|
69
|
+
rubyzip (3.2.2)
|
|
66
70
|
simplecov (0.22.0)
|
|
67
71
|
docile (~> 1.1)
|
|
68
72
|
simplecov-html (~> 0.11)
|
|
69
73
|
simplecov_json_formatter (~> 0.1)
|
|
70
74
|
simplecov-html (0.13.2)
|
|
71
75
|
simplecov_json_formatter (0.1.4)
|
|
72
|
-
stringio (3.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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)
|
|
78
83
|
|
|
79
84
|
PLATFORMS
|
|
80
85
|
aarch64-linux-gnu
|
|
@@ -87,14 +92,14 @@ PLATFORMS
|
|
|
87
92
|
x86_64-linux-musl
|
|
88
93
|
|
|
89
94
|
DEPENDENCIES
|
|
90
|
-
minitest (~>
|
|
95
|
+
minitest (~> 6.0)
|
|
91
96
|
nokogiri (~> 1.18)
|
|
92
97
|
rake (~> 13.2)
|
|
93
|
-
rdoc (~>
|
|
98
|
+
rdoc (~> 7.2)
|
|
94
99
|
rubocop (~> 1.75)
|
|
95
|
-
rubocop-minitest (~> 0.
|
|
100
|
+
rubocop-minitest (~> 0.39.0)
|
|
96
101
|
rubocop-rake (~> 0.7.1)
|
|
97
|
-
rubyzip (~> 2
|
|
102
|
+
rubyzip (~> 3.2)
|
|
98
103
|
simplecov
|
|
99
104
|
webrick (~> 1.9)
|
|
100
105
|
yard (~> 0.9.37)
|
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.
|
|
@@ -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
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EpubTools
|
|
4
|
+
module CLI
|
|
5
|
+
# Handles command-specific option configuration for CLI commands
|
|
6
|
+
class CommandOptionsConfigurator
|
|
7
|
+
# Configure command-specific options using dynamic dispatch
|
|
8
|
+
# @param cmd [String] Command name
|
|
9
|
+
# @param builder [OptionBuilder] Option builder instance
|
|
10
|
+
def configure(cmd, builder)
|
|
11
|
+
method_name = "configure_#{cmd.tr('-', '_')}_options"
|
|
12
|
+
raise ArgumentError, "Unknown command: #{cmd}" unless respond_to?(method_name, true)
|
|
13
|
+
|
|
14
|
+
send(method_name, builder)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
# Configure options for the 'add' command
|
|
20
|
+
# @param builder [OptionBuilder] Option builder instance
|
|
21
|
+
def configure_add_options(builder)
|
|
22
|
+
builder.with_custom_options do |opts, options|
|
|
23
|
+
opts.on('-c DIR', '--chapters-dir DIR', 'Chapters directory (required)') { |v| options[:chapters_dir] = v }
|
|
24
|
+
opts.on('-e DIR', '--oebps-dir DIR', 'EPUB OEBPS directory (required)') { |v| options[:oebps_dir] = v }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Configure options for the 'extract' command
|
|
29
|
+
# @param builder [OptionBuilder] Option builder instance
|
|
30
|
+
def configure_extract_options(builder)
|
|
31
|
+
builder.with_custom_options do |opts, options|
|
|
32
|
+
opts.on('-s DIR', '--source-dir DIR', 'Directory with EPUBs to extract XHTMLs from (required)') do |v|
|
|
33
|
+
options[:source_dir] = v
|
|
34
|
+
end
|
|
35
|
+
opts.on('-t DIR', '--target-dir DIR',
|
|
36
|
+
'Directory where the XHTML files will be extracted to (required)') do |v|
|
|
37
|
+
options[:target_dir] = v
|
|
38
|
+
end
|
|
39
|
+
end.with_verbose_option
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Configure options for the 'split' command
|
|
43
|
+
# @param builder [OptionBuilder] Option builder instance
|
|
44
|
+
def configure_split_options(builder)
|
|
45
|
+
builder.with_custom_options do |opts, options|
|
|
46
|
+
add_split_input_options(opts, options)
|
|
47
|
+
add_split_output_options(opts, options)
|
|
48
|
+
end.with_verbose_option
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def add_split_input_options(opts, options)
|
|
52
|
+
opts.on('-i FILE', '--input FILE', 'Source XHTML file (required)') { |v| options[:input_file] = v }
|
|
53
|
+
opts.on('-t TITLE', '--title TITLE', 'Book title for HTML <title> tags (required)') do |v|
|
|
54
|
+
options[:book_title] = v
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def add_split_output_options(opts, options)
|
|
59
|
+
opts.on('-o DIR', '--output-dir DIR',
|
|
60
|
+
"Output directory for chapter files (default: #{options[:output_dir]})") do |v|
|
|
61
|
+
options[:output_dir] = v
|
|
62
|
+
end
|
|
63
|
+
opts.on('-p PREFIX', '--prefix PREFIX', "Filename prefix for chapters (default: #{options[:prefix]})") do |v|
|
|
64
|
+
options[:prefix] = v
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Configure options for the 'init' command
|
|
69
|
+
# @param builder [OptionBuilder] Option builder instance
|
|
70
|
+
def configure_init_options(builder)
|
|
71
|
+
builder.with_title_option
|
|
72
|
+
.with_author_option
|
|
73
|
+
.with_custom_options do |opts, options|
|
|
74
|
+
opts.on('-o DIR', '--output-dir DIR', 'Destination EPUB directory (required)') do |v|
|
|
75
|
+
options[:destination] = v
|
|
76
|
+
end
|
|
77
|
+
end.with_cover_option
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Configure options for the 'pack' command
|
|
81
|
+
# @param builder [OptionBuilder] Option builder instance
|
|
82
|
+
def configure_pack_options(builder)
|
|
83
|
+
builder.with_input_dir('EPUB directory to package')
|
|
84
|
+
.with_output_file('Output EPUB file path')
|
|
85
|
+
.with_verbose_option
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Configure options for the 'unpack' command
|
|
89
|
+
# @param builder [OptionBuilder] Option builder instance
|
|
90
|
+
def configure_unpack_options(builder)
|
|
91
|
+
builder.with_custom_options do |opts, options|
|
|
92
|
+
opts.on('-i FILE', '--input-file FILE', 'EPUB file to unpack (required)') { |v| options[:epub_file] = v }
|
|
93
|
+
opts.on('-o DIR', '--output-dir DIR', 'Output directory to extract into (default: basename of epub)') do |v|
|
|
94
|
+
options[:output_dir] = v
|
|
95
|
+
end
|
|
96
|
+
end.with_verbose_option
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Configure options for the 'compile' command
|
|
100
|
+
# @param builder [OptionBuilder] Option builder instance
|
|
101
|
+
def configure_compile_options(builder)
|
|
102
|
+
builder.with_title_option
|
|
103
|
+
.with_author_option
|
|
104
|
+
.with_custom_options do |opts, options|
|
|
105
|
+
opts.on('-s DIR', '--source-dir DIR', 'Directory with EPUBs to extract XHTMLs from (required)') do |v|
|
|
106
|
+
options[:source_dir] = v
|
|
107
|
+
end
|
|
108
|
+
opts.on('-o FILE', '--output FILE', 'EPUB to create (default: book title in source dir)') do |v|
|
|
109
|
+
options[:output_file] = v
|
|
110
|
+
end
|
|
111
|
+
end.with_cover_option.with_verbose_option
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'optparse'
|
|
2
4
|
|
|
3
5
|
module EpubTools
|
|
@@ -45,7 +47,7 @@ module EpubTools
|
|
|
45
47
|
# @param description [String] Option description
|
|
46
48
|
# @param required [Boolean] Whether this option is required
|
|
47
49
|
# @return [self] for method chaining
|
|
48
|
-
def with_input_file(description = 'Input file', required
|
|
50
|
+
def with_input_file(description = 'Input file', required: true)
|
|
49
51
|
desc = required ? "#{description} (required)" : description
|
|
50
52
|
@parser.on('-i FILE', '--input-file FILE', desc) { |v| @options[:input_file] = v }
|
|
51
53
|
self
|
|
@@ -55,7 +57,7 @@ module EpubTools
|
|
|
55
57
|
# @param description [String] Option description
|
|
56
58
|
# @param required [Boolean] Whether this option is required
|
|
57
59
|
# @return [self] for method chaining
|
|
58
|
-
def with_input_dir(description = 'Input directory', required
|
|
60
|
+
def with_input_dir(description = 'Input directory', required: true)
|
|
59
61
|
desc = required ? "#{description} (required)" : description
|
|
60
62
|
@parser.on('-i DIR', '--input-dir DIR', desc) { |v| @options[:input_dir] = v }
|
|
61
63
|
self
|
|
@@ -80,7 +82,7 @@ module EpubTools
|
|
|
80
82
|
# @param description [String] Option description
|
|
81
83
|
# @param required [Boolean] Whether this option is required
|
|
82
84
|
# @return [self] for method chaining
|
|
83
|
-
def with_output_file(description = 'Output file', required
|
|
85
|
+
def with_output_file(description = 'Output file', required: true)
|
|
84
86
|
desc = required ? "#{description} (required)" : description
|
|
85
87
|
@parser.on('-o FILE', '--output-file FILE', desc) { |v| @options[:output_file] = v }
|
|
86
88
|
self
|