epub_tools 0.2.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ea6ef571bfc4a850094975f5bf56be8d56b4a81b5cf977dc7a6fca295005086a
4
+ data.tar.gz: 37b32d7759ac7b00f273871558778cfe0c613955e7319dbee8f33e590678eb94
5
+ SHA512:
6
+ metadata.gz: 9ea5b766003d9595f9d067e4dbfea5a6348102deb29092be235fadb3b511feec1dff20c1bc63683673378e1136e70f026c7a747c54793a329e7a136b9d18b4d3
7
+ data.tar.gz: 411cf6f8b5e7cde261368076e55ada115df9f8884bf951b0b3ea2a2ab0fa77425ecf2a04e3de63e3c1351f2bd862c0bb7f0296781c1e78ed9d7c4cea73e02e76
@@ -0,0 +1,21 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["main"]
6
+ pull_request:
7
+ branches: ["main"]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v3
14
+ - uses: ruby/setup-ruby@v1
15
+ with:
16
+ ruby-version: '3.4.3'
17
+ bundler-cache: true
18
+ - name: Install dependencies
19
+ run: bundle install --jobs 4 --retry 3
20
+ - name: Run tests
21
+ run: bundle exec rake test
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ .DS_Store
2
+ /.bundle/
3
+ /coverage/
4
+ vendor/bundle/
5
+ *.gem
6
+
7
+ text_style_classes.yaml
@@ -0,0 +1,4 @@
1
+ {
2
+ "workspace.art_style" : 0,
3
+ "workspace.name" : "EPub Tools"
4
+ }
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.4.3
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ ruby '3.4.3'
4
+
5
+ source "https://rubygems.org"
6
+
7
+ gem "nokogiri", "~> 1.18"
8
+ gem "rake", "~> 13.2"
9
+ gem "rubyzip", "~> 2.4"
10
+
11
+ group :test do
12
+ gem "minitest", "~> 5.25"
13
+ gem "simplecov", require: false
14
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,53 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ docile (1.4.1)
5
+ minitest (5.25.5)
6
+ nokogiri (1.18.8-aarch64-linux-gnu)
7
+ racc (~> 1.4)
8
+ nokogiri (1.18.8-aarch64-linux-musl)
9
+ racc (~> 1.4)
10
+ nokogiri (1.18.8-arm-linux-gnu)
11
+ racc (~> 1.4)
12
+ nokogiri (1.18.8-arm-linux-musl)
13
+ racc (~> 1.4)
14
+ nokogiri (1.18.8-arm64-darwin)
15
+ racc (~> 1.4)
16
+ nokogiri (1.18.8-x86_64-darwin)
17
+ racc (~> 1.4)
18
+ nokogiri (1.18.8-x86_64-linux-gnu)
19
+ racc (~> 1.4)
20
+ nokogiri (1.18.8-x86_64-linux-musl)
21
+ racc (~> 1.4)
22
+ racc (1.8.1)
23
+ rake (13.2.1)
24
+ rubyzip (2.4.1)
25
+ simplecov (0.22.0)
26
+ docile (~> 1.1)
27
+ simplecov-html (~> 0.11)
28
+ simplecov_json_formatter (~> 0.1)
29
+ simplecov-html (0.13.1)
30
+ simplecov_json_formatter (0.1.4)
31
+
32
+ PLATFORMS
33
+ aarch64-linux-gnu
34
+ aarch64-linux-musl
35
+ arm-linux-gnu
36
+ arm-linux-musl
37
+ arm64-darwin
38
+ x86_64-darwin
39
+ x86_64-linux-gnu
40
+ x86_64-linux-musl
41
+
42
+ DEPENDENCIES
43
+ minitest (~> 5.25)
44
+ nokogiri (~> 1.18)
45
+ rake (~> 13.2)
46
+ rubyzip (~> 2.4)
47
+ simplecov
48
+
49
+ RUBY VERSION
50
+ ruby 3.4.3p32
51
+
52
+ BUNDLED WITH
53
+ 2.6.7
data/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # EPUB Tools
2
+
3
+ **TL;DR:** A Ruby gem and CLI for working with EPUB files: extract, split, initialize, add chapters, pack, and unpack EPUB books.
4
+
5
+ ## Installation
6
+ Install the gem via RubyGems:
7
+ ```bash
8
+ gem install epub_tools
9
+ ```
10
+
11
+ Or build and install locally:
12
+ ```bash
13
+ bundle install
14
+ gem build epub_tools.gemspec
15
+ gem install ./epub_tools-*.gem
16
+ ```
17
+
18
+ ## CLI Usage
19
+ After installation, use the `epub-tools` executable:
20
+
21
+ ```bash
22
+ Usage: epub-tools COMMAND [options]
23
+ ```
24
+
25
+ Commands:
26
+ - `init` Initialize a new EPUB directory structure
27
+ - `extract` Extract XHTML files from EPUB archives
28
+ - `split` Split an XHTML file into separate chapter files
29
+ - `add` Add chapter XHTML files into an existing EPUB
30
+ - `pack` Package an EPUB directory into a `.epub` file
31
+ - `unpack` Unpack a `.epub` file into a directory
32
+ - `compile` Takes EPUBs in a dir and splits, cleans, and compiles into a single EPUB
33
+
34
+ Run `epub-tools COMMAND --help` for details on options.
35
+
36
+ ### Example
37
+ ```bash
38
+ # Extract XHTMLs
39
+ epub-tools extract -s source_epubs -t xhtml_output
40
+
41
+ # Split chapters
42
+ epub-tools split -i xhtml_output/chapter1.xhtml -t "My Book" -o chapters
43
+
44
+ # Initialize EPUB
45
+ epub-tools init -t "My Book" -a "Author Name" -o epub_dir -c cover.jpg
46
+
47
+ # Add chapters to EPUB
48
+ epub-tools add -c chapters -e epub_dir/OEBPS
49
+
50
+ # Package EPUB (Ruby)
51
+ epub-tools pack -i epub_dir -o MyBook.epub
52
+
53
+ # Unpack EPUB
54
+ epub-tools unpack -i MyBook.epub -o unpacked_dir
55
+
56
+ # Full compile workflow: extract, split, initialize, add, and pack into one EPUB
57
+ epub-tools compile -t "My Book" -a "Author Name" -s source_epubs -c cover.jpg -o MyBook.epub
58
+ ```
59
+
60
+ (Legacy script references removed; see CLI Usage above)
61
+
62
+ ## Library Usage
63
+ Use the library directly in Ruby:
64
+ ```ruby
65
+ require 'epub_tools'
66
+
67
+ # Extract XHTML
68
+ EpubTools::XHTMLExtractor.new(
69
+ source_dir: 'source_epubs',
70
+ target_dir: 'xhtml_output',
71
+ verbose: true
72
+ ).extract_all
73
+
74
+ # Split chapters
75
+ EpubTools::SplitChapters.new(
76
+ 'xhtml_output/chapter1.xhtml',
77
+ 'My Book',
78
+ 'chapters',
79
+ 'chapter'
80
+ ).run
81
+
82
+ # Initialize EPUB
83
+ EpubTools::EpubInitializer.new(
84
+ 'My Book',
85
+ 'Author Name',
86
+ 'epub_dir',
87
+ 'cover.jpg'
88
+ ).run
89
+
90
+ # Add chapters
91
+ EpubTools::AddChaptersToEpub.new('chapters', 'epub_dir/OEBPS').run
92
+
93
+ # Pack EPUB
94
+ EpubTools::PackEbook.new('epub_dir', 'MyBook.epub').run
95
+
96
+ # Unpack EPUB
97
+ EpubTools::UnpackEbook.new('MyBook.epub', 'unpacked_dir').run
98
+ ```
99
+ ## Development & Testing
100
+ Clone the repo and install dependencies:
101
+ ```bash
102
+ git clone <repo-url>
103
+ cd epub_tools
104
+ bundle install
105
+ ```
106
+
107
+ Run tests:
108
+ ```bash
109
+ bundle exec rake test
110
+ ```
111
+
112
+ Enable coverage reporting:
113
+ ```bash
114
+ COVERAGE=true bundle exec rake test
115
+ ```
116
+
117
+ ## Contributing
118
+ Pull requests welcome! Please open an issue for major changes.
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'rake/testtask'
2
+
3
+ desc 'Run all tests'
4
+ Rake::TestTask.new do |t|
5
+ t.libs << 'test'
6
+ t.pattern = 'test/**/*_test.rb'
7
+ end
8
+
9
+ task default: :test
data/bin/epub-tools ADDED
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative '../lib/epub_tools'
3
+
4
+ prog = File.basename($PROGRAM_NAME)
5
+ script_dir = File.expand_path(File.join(__dir__, '..'))
6
+ commands = %w[add extract init split pack unpack compile]
7
+
8
+ if ARGV.empty? || !commands.include?(ARGV[0])
9
+ puts <<~USAGE
10
+ Usage: #{prog} COMMAND [options]
11
+ Commands:
12
+ init Initialize a bare-bones EPUB
13
+ extract Extract XHTML files from EPUBs
14
+ split Split XHTML into separate XHTMLs per chapter
15
+ add Add chapter XHTML files into an EPUB
16
+ pack Package an EPUB directory into a .epub file
17
+ unpack Unpack an EPUB file into a directory
18
+ compile Takes EPUBs in a dir and splits, cleans, and compiles into a single EPUB.
19
+ USAGE
20
+ exit 1
21
+ end
22
+
23
+ cmd = ARGV.shift
24
+
25
+ case cmd
26
+ when 'add'
27
+ options = {}
28
+ EpubTools::CLIHelper.parse(options, [:chapters_dir, :epub_oebps_dir]) do |opts, o|
29
+ opts.banner = "Usage: #{prog} add [options]"
30
+ opts.on('-c DIR', '--chapters-dir DIR', 'Chapters directory (required)') { |v| o[:chapters_dir] = v }
31
+ opts.on('-e DIR', '--epub-oebps-dir DIR', 'EPUB OEBPS directory (required)') { |v| o[:epub_oebps_dir] = v }
32
+ end
33
+
34
+ EpubTools::AddChaptersToEpub.new(options[:chapters_dir], options[:epub_oebps_dir]).run
35
+
36
+ when 'extract'
37
+ options = { verbose: true }
38
+ EpubTools::CLIHelper.parse(options, [:source_dir, :target_dir]) do |opts, o|
39
+ opts.banner = "Usage: #{prog} extract [options]"
40
+ opts.on('-s DIR', '--source-dir DIR', 'Directory with EPUBs to extract XHTMLs from (required)') { |v| o[:source_dir] = v }
41
+ opts.on('-t DIR', '--target-dir DIR', 'Directory where the XHTML files will be extracted to (required)') { |v| o[:target_dir] = v }
42
+ opts.on('-q', '--quiet', 'Run quietly (default: verbose)') { |v| o[:verbose] = !v }
43
+ end
44
+ EpubTools::XHTMLExtractor.new(
45
+ source_dir: options[:source_dir],
46
+ target_dir: options[:target_dir],
47
+ verbose: options[:verbose]
48
+ ).extract_all
49
+
50
+ when 'split'
51
+ options = { output_dir: './chapters', prefix: 'chapter', verbose: true }
52
+ EpubTools::CLIHelper.parse(options, [:input_file, :book_title]) do |opts, o|
53
+ opts.banner = "Usage: #{prog} split [options]"
54
+ opts.on('-i FILE', '--input FILE', 'Source XHTML file (required)') { |v| options[:input_file] = v }
55
+ opts.on('-t TITLE', '--title TITLE', 'Book title for HTML <title> tags (required)') { |v| options[:book_title] = v }
56
+ opts.on('-o DIR', '--output-dir DIR', "Output directory for chapter files (default: #{options[:output_dir]})") { |v| options[:output_dir] = v }
57
+ opts.on('-p PREFIX', '--prefix PREFIX', "Filename prefix for chapters (default: #{options[:prefix]})") { |v| options[:prefix] = v }
58
+ opts.on('-q', '--quiet', 'Run quietly (default: verbose)') { |v| o[:verbose] = !v }
59
+ end
60
+ EpubTools::SplitChapters.new(options[:input_file], options[:book_title], options[:output_dir], options[:prefix], options[:verbose]).run
61
+
62
+ when 'init'
63
+ options = {}
64
+ EpubTools::CLIHelper.parse(options, [:title, :author, :destination]) do |opts, o|
65
+ opts.banner = "Usage: #{prog} init [options]"
66
+ opts.on('-t TITLE', '--title TITLE', 'Book title (required)') { |v| o[:title] = v }
67
+ opts.on('-a AUTHOR', '--author AUTHOR', 'Author name (required)') { |v| o[:author] = v }
68
+ opts.on('-o DIR', '--output-dir DIR', 'Destination EPUB directory (required)') { |v| o[:destination] = v }
69
+ opts.on('-c PATH', '--cover PATH', 'Cover image file path (optional)') { |v| o[:cover_image] = v }
70
+ end
71
+
72
+ EpubTools::EpubInitializer.new(options[:title], options[:author], options[:destination], options[:cover_image]).run
73
+
74
+ when 'pack'
75
+ options = {verbose: true}
76
+ EpubTools::CLIHelper.parse(options, [:input_dir, :output_file]) do |opts, o|
77
+ opts.banner = "Usage: #{prog} pack [options]"
78
+ opts.on('-i DIR', '--input-dir DIR', 'EPUB directory to package (required)') { |v| o[:input_dir] = v }
79
+ opts.on('-o FILE', '--output-file FILE', 'Output EPUB file path (required)') { |v| o[:output_file] = v }
80
+ opts.on('-q', '--quiet', 'Run quietly (default: verbose)') { |v| o[:verbose] = !v }
81
+ end
82
+
83
+ EpubTools::PackEbook.new(options[:input_dir], options[:output_file], verbose: options[:verbose]).run
84
+
85
+ when 'unpack'
86
+ options = {verbose: true}
87
+ EpubTools::CLIHelper.parse(options, [:epub_file]) do |opts, o|
88
+ opts.banner = "Usage: #{prog} unpack [options]"
89
+ opts.on('-i FILE', '--input-file FILE', 'EPUB file to unpack (required)') { |v| o[:epub_file] = v }
90
+ opts.on('-o DIR', '--output-dir DIR', 'Output directory to extract into (default: basename of epub)') { |v| o[:output_dir] = v }
91
+ opts.on('-q', '--quiet', 'Run quietly (default: verbose)') { |v| o[:verbose] = !v }
92
+ end
93
+ EpubTools::UnpackEbook.new(options[:epub_file], verbose: options[:verbose]).run
94
+
95
+ when 'compile'
96
+ options = {verbose: true}
97
+ EpubTools::CLIHelper.parse(options, %i(title author source_dir)) do |opts, o|
98
+ opts.banner = "Usage: #{prog} compile [options]"
99
+ opts.on('-t TITLE', '--title TITLE', 'Book title (required)') { |v| o[:title] = v }
100
+ opts.on('-a AUTHOR', '--author AUTHOR', 'Author name (required)') { |v| o[:author] = v }
101
+ opts.on('-s DIR', '--source-dir DIR', 'Directory with EPUBs to extract XHTMLs from (required)') { |v| o[:source_dir] = v }
102
+ opts.on('-o FILE', '--output FILE', 'EPUB to create (default: book title in source dir)') { |v| o[:output_file] = v }
103
+ opts.on('-c PATH', '--cover PATH', 'Cover image file path (optional)') { |v| o[:cover_image] = v }
104
+ opts.on('-q', '--quiet', 'Run quietly (default: verbose)') { |v| o[:verbose] = !v }
105
+ end
106
+ EpubTools::CompileBook.new(*options).run
107
+ end
@@ -0,0 +1,21 @@
1
+ require_relative 'lib/epub_tools/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'epub_tools'
5
+ spec.version = EpubTools::VERSION
6
+ spec.summary = 'Tools to extract, split, and compile EPUB books'
7
+ spec.authors = ['Jaime Rodas']
8
+ spec.email = ['rodas@hey.com']
9
+ spec.license = 'MIT'
10
+ spec.files = `git ls-files`.split("\n")
11
+ spec.require_paths = ['lib']
12
+ spec.executables = ['epub-tools']
13
+ spec.required_ruby_version = ">= 3.0"
14
+
15
+ spec.add_dependency 'nokogiri', '~> 1.18'
16
+ spec.add_dependency 'rubyzip', '~> 2.4'
17
+ spec.add_dependency 'rake', '~> 13.2'
18
+
19
+ spec.add_development_dependency 'minitest', '~> 5.25'
20
+ spec.add_development_dependency 'simplecov', '~> 0'
21
+ end
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env ruby
2
+ require 'nokogiri'
3
+ require 'fileutils'
4
+
5
+ module EpubTools
6
+ class AddChaptersToEpub
7
+ def initialize(chapters_dir = './chapters', epub_dir = './epub/OEBPS', verbose = false)
8
+ @chapters_dir = chapters_dir
9
+ @epub_dir = epub_dir
10
+ @opf_file = File.join(@epub_dir, 'package.opf')
11
+ @nav_file = File.join(@epub_dir, 'nav.xhtml')
12
+ @verbose = verbose
13
+ end
14
+
15
+ def run
16
+ moved_files = move_chapters
17
+ update_package_opf(moved_files)
18
+ update_nav_xhtml(moved_files)
19
+ @verbose ? moved_files.each {|f| puts "Moved: #{f}"} : moved_files
20
+ end
21
+
22
+ private
23
+
24
+ def move_chapters
25
+ # Sort by chapter number (numeric)
26
+ chapter_files = Dir.glob(File.join(@chapters_dir, '*.xhtml')).sort_by do |path|
27
+ # extract first integer from filename (e.g. chapter_10.xhtml -> 10)
28
+ File.basename(path)[/\d+/].to_i
29
+ end
30
+ chapter_files.each do |file|
31
+ FileUtils.mv(file, @epub_dir)
32
+ end
33
+ chapter_files.map { |f| File.basename(f) }
34
+ end
35
+
36
+ def chapter_id(filename)
37
+ match = filename.match(/chapter_(\d+)\.xhtml/)
38
+ match ? "chap#{match[1]}" : File.basename(filename, '.xhtml')
39
+ end
40
+
41
+ def update_package_opf(filenames)
42
+ doc = Nokogiri::XML(File.read(@opf_file)) { |config| config.default_xml.noblanks }
43
+ manifest = doc.at_xpath('//xmlns:manifest')
44
+ spine = doc.at_xpath('//xmlns:spine')
45
+
46
+ filenames.each do |filename|
47
+ id = chapter_id(filename)
48
+ # Add <item> to the manifest if missing
49
+ unless doc.at_xpath("//xmlns:item[@href='#{filename}']")
50
+ item = Nokogiri::XML::Node.new('item', doc)
51
+ item['id'] = id
52
+ item['href'] = filename
53
+ item['media-type'] = 'application/xhtml+xml'
54
+ manifest.add_child(item)
55
+ end
56
+
57
+ # Add <itemref> to the spine if missing
58
+ unless doc.at_xpath("//xmlns:itemref[@idref='#{id}']")
59
+ itemref = Nokogiri::XML::Node.new('itemref', doc)
60
+ itemref['idref'] = id
61
+ spine.add_child(itemref)
62
+ end
63
+ end
64
+
65
+ File.write(@opf_file, doc.to_xml(indent: 2))
66
+ end
67
+
68
+ def update_nav_xhtml(filenames)
69
+ doc = Nokogiri::XML(File.read(@nav_file)) { |config| config.default_xml.noblanks }
70
+ nav = doc.at_xpath('//xmlns:nav[@epub:type="toc"]/xmlns:ol')
71
+
72
+ filenames.each do |filename|
73
+ # Create a new <li><a href="...">Label</a></li> element
74
+ label = File.basename(filename, '.xhtml').gsub('_', ' ').capitalize
75
+ label = "Prologue" if label == "Chapter 0"
76
+ li = Nokogiri::XML::Node.new('li', doc)
77
+ a = Nokogiri::XML::Node.new('a', doc)
78
+ a['href'] = filename
79
+ a.content = label
80
+ li.add_child(a)
81
+ nav.add_child(li)
82
+ end
83
+
84
+ File.write(@nav_file, doc.to_xml(indent: 2))
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,31 @@
1
+ require 'optparse'
2
+
3
+ module EpubTools
4
+ # A simple helper to DRY CLI OptionParser usage across commands
5
+ class CLIHelper
6
+ # Parses ARGV into options hash, enforces required keys, and displays help/errors.
7
+ # options: hash of defaults; required_keys: array of symbols required
8
+ def self.parse(options = {}, required_keys = [], &block)
9
+ parser = OptionParser.new do |opts|
10
+ block.call(opts, options)
11
+ opts.on('-h', '--help', 'Prints this help') { puts opts; exit }
12
+ end
13
+ begin
14
+ parser.parse!
15
+ unless required_keys.empty?
16
+ missing = required_keys.select { |k| options[k].nil? }
17
+ unless missing.empty?
18
+ STDERR.puts "Missing required options: #{missing.map { |k| "--#{k.to_s.gsub('_','-')}" }.join(', ')}"
19
+ STDERR.puts parser
20
+ exit 1
21
+ end
22
+ end
23
+ rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
24
+ STDERR.puts e.message
25
+ STDERR.puts parser
26
+ exit 1
27
+ end
28
+ options
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env ruby
2
+ require 'fileutils'
3
+ $LOAD_PATH.unshift File.expand_path('../../', __FILE__)
4
+ require 'epub_tools'
5
+
6
+ module EpubTools
7
+ # Orchestrates extraction, splitting, validation, and packaging of book EPUBs
8
+ class CompileBook
9
+ attr_reader :title, :author, :source_dir, :cover_image, :output_file, :build_dir, :verbose
10
+
11
+ # title: String, author: String, source_dir: path to input epubs
12
+ # cover_image: optional path to cover image, output_file: filename for final epub
13
+ # build_dir: optional working directory for intermediate files
14
+ def initialize(title:, author:, source_dir:, cover_image: nil, output_file: nil, build_dir: nil, verbose: false)
15
+ @title = title
16
+ @author = author
17
+ @source_dir = source_dir
18
+ @cover_image = cover_image
19
+ @output_file = output_file || default_output_file
20
+ @build_dir = build_dir || File.join(Dir.pwd, '.epub_tools_build')
21
+ @verbose = verbose
22
+ end
23
+
24
+ # Run the full compile workflow
25
+ def run
26
+ clean_build_dir
27
+ prepare_dirs
28
+ extract_xhtmls
29
+ split_xhtmls
30
+ validate_sequence
31
+ initialize_epub
32
+ add_chapters
33
+ pack_epub
34
+ log "Done. Output EPUB: #{File.expand_path(output_file)}"
35
+ clean_build_dir
36
+ end
37
+
38
+ private
39
+
40
+ def log(message)
41
+ puts message if verbose
42
+ end
43
+
44
+ def default_output_file
45
+ "#{title.gsub(' ', '_')}.epub"
46
+ end
47
+
48
+ def clean_build_dir
49
+ log "Cleaning build directory #{build_dir}..."
50
+ FileUtils.rm_rf(build_dir)
51
+ end
52
+
53
+ def prepare_dirs
54
+ log "Preparing build directories..."
55
+ FileUtils.mkdir_p(xhtml_dir)
56
+ FileUtils.mkdir_p(chapters_dir)
57
+ end
58
+
59
+ def xhtml_dir
60
+ @xhtml_dir ||= File.join(build_dir, 'xhtml')
61
+ end
62
+
63
+ def chapters_dir
64
+ @chapters_dir ||= File.join(build_dir, 'chapters')
65
+ end
66
+
67
+ def epub_dir
68
+ @epub_dir ||= File.join(build_dir, 'epub')
69
+ end
70
+
71
+ def extract_xhtmls
72
+ log "Extracting XHTML files from epubs in '#{source_dir}'..."
73
+ XHTMLExtractor.new(source_dir: source_dir, target_dir: xhtml_dir, verbose: verbose).extract_all
74
+ end
75
+
76
+ def split_xhtmls
77
+ log "Splitting XHTML files into chapters..."
78
+ Dir.glob(File.join(xhtml_dir, '*.xhtml')).each do |xhtml_file|
79
+ base = File.basename(xhtml_file, '.xhtml')
80
+ log "Splitting '#{base}'..."
81
+ SplitChapters.new(xhtml_file, title, chapters_dir, 'chapter', verbose).run
82
+ end
83
+ end
84
+
85
+ def validate_sequence
86
+ log "Validating chapter sequence..."
87
+ nums = Dir.glob(File.join(chapters_dir, '*.xhtml')).map do |file|
88
+ if (m = File.basename(file, '.xhtml').match(/_(\d+)\z/))
89
+ m[1].to_i
90
+ end
91
+ end.compact
92
+ raise "No chapter files found in #{chapters_dir}" if nums.empty?
93
+ sorted = nums.sort.uniq
94
+ missing = (sorted.first..sorted.last).to_a - sorted
95
+ if missing.any?
96
+ raise "Missing chapter numbers: #{missing.join(' ')}"
97
+ else
98
+ log "Chapter sequence is complete: #{sorted.first} to #{sorted.last}."
99
+ end
100
+ end
101
+
102
+ def initialize_epub
103
+ log "Initializing new EPUB..."
104
+ if cover_image
105
+ EpubInitializer.new(title, author, epub_dir, cover_image).run
106
+ else
107
+ EpubInitializer.new(title, author, epub_dir).run
108
+ end
109
+ end
110
+
111
+ def add_chapters
112
+ log "Adding chapters to EPUB..."
113
+ AddChaptersToEpub.new(chapters_dir, File.join(epub_dir, 'OEBPS'), verbose).run
114
+ end
115
+
116
+ def pack_epub
117
+ log "Building final EPUB '#{output_file}'..."
118
+ PackEbook.new(epub_dir, output_file, verbose: verbose).run
119
+ end
120
+ end
121
+ end