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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +3 -0
  3. data/.gitignore +1 -1
  4. data/.rubocop.yml +10 -17
  5. data/.tool-versions +1 -0
  6. data/CLAUDE.md +124 -0
  7. data/Gemfile +4 -4
  8. data/Gemfile.lock +44 -37
  9. data/README.md +10 -6
  10. data/Rakefile +2 -0
  11. data/bin/epub-tools +2 -0
  12. data/epub_tools.gemspec +3 -1
  13. data/lib/epub_tools/add_chapters.rb +48 -30
  14. data/lib/epub_tools/chapter_validator.rb +40 -0
  15. data/lib/epub_tools/cli/command_options_configurator.rb +115 -0
  16. data/lib/epub_tools/cli/command_registry.rb +2 -0
  17. data/lib/epub_tools/cli/option_builder.rb +5 -3
  18. data/lib/epub_tools/cli/runner.rb +59 -110
  19. data/lib/epub_tools/cli.rb +16 -29
  20. data/lib/epub_tools/compile_book.rb +48 -65
  21. data/lib/epub_tools/compile_workspace.rb +40 -0
  22. data/lib/epub_tools/epub_configuration.rb +33 -0
  23. data/lib/epub_tools/epub_file_writer.rb +57 -0
  24. data/lib/epub_tools/epub_initializer.rb +83 -162
  25. data/lib/epub_tools/epub_metadata_builder.rb +92 -0
  26. data/lib/epub_tools/loggable.rb +2 -0
  27. data/lib/epub_tools/pack_ebook.rb +28 -14
  28. data/lib/epub_tools/split_chapters.rb +42 -17
  29. data/lib/epub_tools/style_finder.rb +17 -6
  30. data/lib/epub_tools/unpack_ebook.rb +20 -10
  31. data/lib/epub_tools/version.rb +3 -1
  32. data/lib/epub_tools/xhtml_cleaner.rb +1 -0
  33. data/lib/epub_tools/xhtml_extractor.rb +20 -10
  34. data/lib/epub_tools/xhtml_generator.rb +71 -0
  35. data/lib/epub_tools.rb +2 -0
  36. data/test/add_chapters_test.rb +50 -26
  37. data/test/chapter_validator_test.rb +47 -0
  38. data/test/cli/command_registry_test.rb +2 -0
  39. data/test/cli/option_builder_test.rb +24 -14
  40. data/test/cli/runner_test.rb +15 -15
  41. data/test/cli_commands_test.rb +3 -1
  42. data/test/cli_test.rb +2 -0
  43. data/test/cli_version_test.rb +2 -0
  44. data/test/compile_book_test.rb +17 -102
  45. data/test/compile_workspace_test.rb +55 -0
  46. data/test/epub_initializer_test.rb +55 -27
  47. data/test/pack_ebook_test.rb +33 -9
  48. data/test/split_chapters_test.rb +27 -7
  49. data/test/style_finder_test.rb +2 -0
  50. data/test/test_helper.rb +2 -0
  51. data/test/unpack_ebook_test.rb +45 -20
  52. data/test/xhtml_cleaner_test.rb +2 -0
  53. data/test/xhtml_extractor_test.rb +3 -1
  54. metadata +15 -5
  55. data/.ruby-version +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 67e06b8c37f922205445fb328c172184eb14436d876514e306892e17faf767e2
4
- data.tar.gz: b0eb25344c96cca09889bc7854b40f48dca2bc49449583a1259dd7ac30803309
3
+ metadata.gz: 830a3d6d88e106980c19f5c19ea5e0365e39499cc90c0c32ff5c204b7cf24abd
4
+ data.tar.gz: 7898e152850e5e390842e04a56947e2e88623bd1e5b65525561423da7a5ec744
5
5
  SHA512:
6
- metadata.gz: b36393dbb0fcc6888a10a2bdb93d30fe88eb423034aacc7455bdb8338e4a0fccdfc8fac70b9998ccd66c1758956afcef0bd8ecd404aabe5eacfa4326b5f3b847
7
- data.tar.gz: f9b2071dc6a832b178fa77c8982f43f645a9ebed4059cd023863cde8ed6220300df1155cfea39ee35455188b2fcd6672abf56f3682a5f46c606891e4e1bb0347
6
+ metadata.gz: 7b852a22e630ad9177c5adc692326dac517b904c687e6cbe3874b7e1f43347a53cfdd381cb5b66b7feaf6faafae75129510aac6a23f84f4b3e82734ae84dc8ac
7
+ data.tar.gz: 4602e5922dbdf257436445546b14501a0064861af84fea854feb0a10ed2ef651d6eefb8009ce459c7463c50c3383eabe3220a10d856271ad38c49cc768b8da5f
@@ -6,6 +6,9 @@ on:
6
6
  pull_request:
7
7
  branches: [ main ]
8
8
 
9
+ permissions:
10
+ contents: read
11
+
9
12
  jobs:
10
13
  test:
11
14
  runs-on: ubuntu-latest
data/.gitignore CHANGED
@@ -8,4 +8,4 @@ vendor/bundle/
8
8
 
9
9
  text_style_classes.yaml
10
10
 
11
- **/.claude/settings.local.json
11
+ **/.claude/
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
- Enabled: false
21
+ Exclude:
22
+ - 'test/**/*'
24
23
 
25
- Metrics/ClassLength:
26
- Enabled: false
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/PerceivedComplexity:
35
- Enabled: false
36
-
37
- Style/OptionalBooleanParameter:
38
- Enabled: false
28
+ Metrics/ClassLength:
29
+ Exclude:
30
+ - 'test/**/*'
39
31
 
40
32
  Minitest/MultipleAssertions:
41
- Enabled: false
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.4'
9
+ gem 'rubyzip', '~> 3.2'
10
10
 
11
11
  group :test, :development do
12
- gem 'minitest', '~> 5.25'
12
+ gem 'minitest', '~> 6.0'
13
13
  gem 'rubocop', '~> 1.75', require: false
14
- gem 'rubocop-minitest', '~> 0.38.0', require: false
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', '~> 6.13'
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.4.1)
5
+ date (3.5.1)
6
6
  docile (1.4.1)
7
- json (2.11.3)
8
- language_server-protocol (3.17.0.4)
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 (5.25.5)
11
- nokogiri (1.18.8-aarch64-linux-gnu)
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.18.8-aarch64-linux-musl)
17
+ nokogiri (1.19.1-aarch64-linux-musl)
14
18
  racc (~> 1.4)
15
- nokogiri (1.18.8-arm-linux-gnu)
19
+ nokogiri (1.19.1-arm-linux-gnu)
16
20
  racc (~> 1.4)
17
- nokogiri (1.18.8-arm-linux-musl)
21
+ nokogiri (1.19.1-arm-linux-musl)
18
22
  racc (~> 1.4)
19
- nokogiri (1.18.8-arm64-darwin)
23
+ nokogiri (1.19.1-arm64-darwin)
20
24
  racc (~> 1.4)
21
- nokogiri (1.18.8-x86_64-darwin)
25
+ nokogiri (1.19.1-x86_64-darwin)
22
26
  racc (~> 1.4)
23
- nokogiri (1.18.8-x86_64-linux-gnu)
27
+ nokogiri (1.19.1-x86_64-linux-gnu)
24
28
  racc (~> 1.4)
25
- nokogiri (1.18.8-x86_64-linux-musl)
29
+ nokogiri (1.19.1-x86_64-linux-musl)
26
30
  racc (~> 1.4)
27
31
  parallel (1.27.0)
28
- parser (3.3.8.0)
32
+ parser (3.3.10.2)
29
33
  ast (~> 2.4.1)
30
34
  racc
31
- prism (1.4.0)
32
- psych (5.2.3)
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.2.1)
38
- rdoc (6.13.1)
41
+ rake (13.3.1)
42
+ rdoc (7.2.0)
43
+ erb
39
44
  psych (>= 4.0.0)
40
- regexp_parser (2.10.0)
41
- rubocop (1.75.4)
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.44.0, < 2.0)
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.44.1)
58
+ rubocop-ast (1.49.0)
53
59
  parser (>= 3.3.7.2)
54
- prism (~> 1.4)
55
- rubocop-minitest (0.38.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.4.1)
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.1)
74
+ simplecov-html (0.13.2)
69
75
  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)
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 (~> 5.25)
95
+ minitest (~> 6.0)
89
96
  nokogiri (~> 1.18)
90
97
  rake (~> 13.2)
91
- rdoc (~> 6.13)
98
+ rdoc (~> 7.2)
92
99
  rubocop (~> 1.75)
93
- rubocop-minitest (~> 0.38.0)
100
+ rubocop-minitest (~> 0.39.0)
94
101
  rubocop-rake (~> 0.7.1)
95
- rubyzip (~> 2.4)
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.3p32
108
+ ruby 3.4.5p51
102
109
 
103
110
  BUNDLED WITH
104
- 2.6.7
111
+ 2.7.0
data/README.md CHANGED
@@ -1,16 +1,20 @@
1
1
  # EPUB Tools
2
2
 
3
- [![Build Status](https://github.com/jaimerodas/epub_tools/actions/workflows/ci.yml/badge.svg)](https://github.com/jaimerodas/epub_tools/actions) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
3
+ [![Build Status](https://github.com/jaimerodas/epub_tools/actions/workflows/ci.yml/badge.svg)](https://github.com/jaimerodas/epub_tools/actions) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Gem Version](https://badge.fury.io/rb/epub_tools.svg)](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
- Install the gem via RubyGems:
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
- Or build and install locally:
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 <repo-url>
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
- Enable coverage reporting:
118
+ Run linting (RuboCop):
115
119
  ```bash
116
- COVERAGE=true bundle exec rake test
120
+ bundle exec rubocop
117
121
  ```
118
122
  ## Documentation
119
123
 
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rake/testtask'
2
4
 
3
5
  desc 'Run all tests'
data/bin/epub-tools CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
2
4
  require_relative '../lib/epub_tools'
3
5
 
4
6
  # Use the new object-oriented CLI architecture
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.4'
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[:epub_dir] || './epub/OEBPS')
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 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
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 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
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