epub_tools 0.5.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +3 -8
- data/.gitignore +0 -1
- data/.tool-versions +1 -1
- data/CLAUDE.md +20 -24
- data/Gemfile +5 -8
- data/Gemfile.lock +60 -54
- data/README.md +65 -25
- data/bin/epub-tools +1 -3
- data/epub_tools.gemspec +3 -4
- data/lib/epub_tools/add_chapters.rb +26 -14
- data/lib/epub_tools/append_book.rb +74 -0
- data/lib/epub_tools/book_builder.rb +114 -0
- data/lib/epub_tools/chapter_order.rb +33 -0
- data/lib/epub_tools/chapter_validator.rb +20 -7
- data/lib/epub_tools/cli.rb +121 -22
- data/lib/epub_tools/compile_book.rb +12 -126
- data/lib/epub_tools/epub_initializer.rb +93 -99
- data/lib/epub_tools/pack_ebook.rb +2 -14
- data/lib/epub_tools/pdf_converter.rb +113 -0
- data/lib/epub_tools/pdf_layout.rb +116 -0
- data/lib/epub_tools/set_cover.rb +116 -0
- data/lib/epub_tools/split_chapters.rb +35 -70
- data/lib/epub_tools/style_finder.rb +11 -40
- data/lib/epub_tools/unpack_ebook.rb +2 -20
- data/lib/epub_tools/version.rb +1 -1
- data/lib/epub_tools/xhtml_cleaner.rb +4 -7
- data/lib/epub_tools/xhtml_extractor.rb +1 -7
- data/lib/epub_tools.rb +4 -0
- data/test/add_chapters_test.rb +134 -0
- data/test/append_book_test.rb +127 -0
- data/test/chapter_validator_test.rb +39 -0
- data/test/cli_commands_test.rb +9 -0
- data/test/cli_test.rb +40 -3
- data/test/compile_book_test.rb +15 -2
- data/test/epub_initializer_test.rb +18 -0
- data/test/pdf_converter_test.rb +69 -0
- data/test/pdf_layout_test.rb +57 -0
- data/test/set_cover_test.rb +68 -0
- data/test/split_chapters_test.rb +84 -9
- data/test/style_finder_test.rb +3 -9
- data/test/test_helper.rb +1 -1
- data/test/unpack_ebook_test.rb +1 -1
- data/test/xhtml_cleaner_test.rb +1 -4
- metadata +17 -34
- data/lib/epub_tools/cli/command_options_configurator.rb +0 -115
- data/lib/epub_tools/cli/command_registry.rb +0 -49
- data/lib/epub_tools/cli/option_builder.rb +0 -166
- data/lib/epub_tools/cli/runner.rb +0 -113
- data/lib/epub_tools/compile_workspace.rb +0 -40
- data/lib/epub_tools/epub_configuration.rb +0 -33
- data/lib/epub_tools/epub_file_writer.rb +0 -57
- data/lib/epub_tools/epub_metadata_builder.rb +0 -92
- data/lib/epub_tools/xhtml_generator.rb +0 -71
- data/test/cli/command_registry_test.rb +0 -68
- data/test/cli/option_builder_test.rb +0 -183
- data/test/cli/runner_test.rb +0 -91
- data/test/compile_workspace_test.rb +0 -55
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require_relative 'loggable'
|
|
5
|
+
require_relative 'xhtml_extractor'
|
|
6
|
+
require_relative 'split_chapters'
|
|
7
|
+
require_relative 'pdf_converter'
|
|
8
|
+
require_relative 'add_chapters'
|
|
9
|
+
require_relative 'pack_ebook'
|
|
10
|
+
require_relative 'chapter_validator'
|
|
11
|
+
|
|
12
|
+
module EpubTools
|
|
13
|
+
# Base class for book-building workflows (compile and append). Sources can be EPUBs (split into chapters)
|
|
14
|
+
# and/or chapter PDFs (see {PDFConverter}[rdoc-ref:EpubTools::PDFConverter] for the naming convention).
|
|
15
|
+
# Uses template method pattern — subclasses override hooks to customize behavior.
|
|
16
|
+
class BookBuilder
|
|
17
|
+
include Loggable
|
|
18
|
+
|
|
19
|
+
attr_reader :source_dir, :build_dir, :verbose
|
|
20
|
+
|
|
21
|
+
def initialize(options = {})
|
|
22
|
+
@source_dir = options.fetch(:source_dir)
|
|
23
|
+
@build_dir = options[:build_dir] || File.join(Dir.pwd, '.epub_tools_build')
|
|
24
|
+
@verbose = options[:verbose] || false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Run the full build workflow
|
|
28
|
+
# @return [String] Path to the output EPUB file
|
|
29
|
+
def run
|
|
30
|
+
setup_workspace
|
|
31
|
+
prepare_epub
|
|
32
|
+
extract_xhtmls
|
|
33
|
+
split_xhtmls
|
|
34
|
+
convert_pdfs
|
|
35
|
+
validate_chapters
|
|
36
|
+
before_add_chapters
|
|
37
|
+
add_chapters
|
|
38
|
+
pack_epub
|
|
39
|
+
finalize_and_cleanup
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# Hook: called before extract/split to set up the EPUB target
|
|
45
|
+
def prepare_epub; end
|
|
46
|
+
|
|
47
|
+
# Hook: called after validation, before adding chapters
|
|
48
|
+
def before_add_chapters; end
|
|
49
|
+
|
|
50
|
+
# Subclasses also define +book_title+ (used when splitting chapters) and +output_path+ (the EPUB to write)
|
|
51
|
+
|
|
52
|
+
def setup_workspace
|
|
53
|
+
FileUtils.rm_rf(build_dir)
|
|
54
|
+
FileUtils.mkdir_p([xhtml_dir, chapters_dir])
|
|
55
|
+
log 'Preparing build directories...'
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def extract_xhtmls
|
|
59
|
+
log "Extracting XHTML files from EPUBs in '#{source_dir}'..."
|
|
60
|
+
XHTMLExtractor.new(source_dir: source_dir, target_dir: xhtml_dir, verbose: verbose).run
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def split_xhtmls
|
|
64
|
+
Dir.glob(File.join(xhtml_dir, '*.xhtml')).each { |f| split_xhtml_file(f) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def split_xhtml_file(xhtml_file)
|
|
68
|
+
log "Splitting '#{File.basename(xhtml_file, '.xhtml')}'..."
|
|
69
|
+
SplitChapters.new(
|
|
70
|
+
input_file: xhtml_file, book_title: book_title,
|
|
71
|
+
output_dir: chapters_dir, verbose: verbose
|
|
72
|
+
).run
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def convert_pdfs
|
|
76
|
+
return if Dir.glob(File.join(source_dir, '*.pdf')).empty?
|
|
77
|
+
|
|
78
|
+
log "Converting PDFs in '#{source_dir}'..."
|
|
79
|
+
PDFConverter.new(
|
|
80
|
+
source_dir: source_dir, book_title: book_title, output_dir: chapters_dir, verbose: verbose
|
|
81
|
+
).run
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def validate_chapters
|
|
85
|
+
ChapterValidator.new(chapters_dir: chapters_dir, verbose: verbose).validate
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def add_chapters
|
|
89
|
+
log 'Adding chapters to EPUB...'
|
|
90
|
+
AddChapters.new(
|
|
91
|
+
chapters_dir: chapters_dir,
|
|
92
|
+
oebps_dir: epub_oebps_dir,
|
|
93
|
+
verbose: verbose
|
|
94
|
+
).run
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def pack_epub
|
|
98
|
+
log "Building EPUB '#{output_path}'..."
|
|
99
|
+
# Expanded here: PackEbook puts a relative path next to epub_dir, inside the build dir that gets deleted
|
|
100
|
+
PackEbook.new(input_dir: epub_dir, output_file: File.expand_path(output_path), verbose: verbose).run
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def finalize_and_cleanup
|
|
104
|
+
log "Done. Output EPUB: #{File.expand_path(output_path)}"
|
|
105
|
+
FileUtils.rm_rf(build_dir)
|
|
106
|
+
output_path
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def xhtml_dir = File.join(build_dir, 'xhtml')
|
|
110
|
+
def chapters_dir = File.join(build_dir, 'chapters')
|
|
111
|
+
def epub_dir = File.join(build_dir, 'epub')
|
|
112
|
+
def epub_oebps_dir = File.join(epub_dir, 'OEBPS')
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EpubTools
|
|
4
|
+
# Reading order of chapter files: chapter_1, chapter_1_5, chapter_1a, chapter_1b, chapter_2, ..., chapter_epilogue
|
|
5
|
+
module ChapterOrder
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# @param filename [String] A chapter filename, e.g. +chapter_12a.xhtml+
|
|
9
|
+
# @return [Array] A key that sorts chapter filenames in reading order
|
|
10
|
+
def sort_key(filename)
|
|
11
|
+
basename = File.basename(filename, '.xhtml')
|
|
12
|
+
return [Float::INFINITY, ''] if basename == 'chapter_epilogue'
|
|
13
|
+
|
|
14
|
+
if (m = basename.match(/_(\d+)_5\z/))
|
|
15
|
+
[m[1].to_f + 0.5, '']
|
|
16
|
+
else
|
|
17
|
+
[basename[/\d+/].to_f, basename[/\d+([a-z]*)\z/, 1].to_s]
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Inserts +node+ into +parent+ before the first chapter that sorts after +filename+, so a chapter added
|
|
22
|
+
# later (e.g. 12a after 13 is already in the book) still lands in reading order.
|
|
23
|
+
# @yield [element] Each existing child element; returns the filename it points to
|
|
24
|
+
def insert(parent, node, filename)
|
|
25
|
+
key = sort_key(filename)
|
|
26
|
+
later = parent.element_children.find do |el|
|
|
27
|
+
href = yield(el)
|
|
28
|
+
href&.start_with?('chapter_') && (sort_key(href) <=> key) == 1
|
|
29
|
+
end
|
|
30
|
+
later ? later.add_previous_sibling(node) : parent.add_child(node)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -12,28 +12,41 @@ module EpubTools
|
|
|
12
12
|
@verbose = verbose
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
+
# Validates that integer chapter numbers form a complete sequence with no gaps.
|
|
16
|
+
# Half-chapters (e.g. chapter_5_5.xhtml) and lettered side chapters (e.g. chapter_5a.xhtml) are
|
|
17
|
+
# recognized but not required.
|
|
18
|
+
# @raise [RuntimeError] if no chapter files are found or if integer chapters have gaps
|
|
15
19
|
def validate
|
|
16
20
|
log 'Validating chapter sequence...'
|
|
17
21
|
nums = extract_chapter_numbers
|
|
18
22
|
check_sequence_completeness(nums)
|
|
19
|
-
log "Chapter sequence is complete: #{nums.first} to #{nums.last}."
|
|
23
|
+
log "Chapter sequence is complete: #{nums.first.floor} to #{nums.last.floor}."
|
|
20
24
|
end
|
|
21
25
|
|
|
22
26
|
private
|
|
23
27
|
|
|
24
28
|
def extract_chapter_numbers
|
|
25
|
-
nums = Dir.glob(File.join(@chapters_dir, '*.xhtml')).
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
end
|
|
29
|
-
end.compact
|
|
29
|
+
nums = Dir.glob(File.join(@chapters_dir, '*.xhtml')).filter_map do |file|
|
|
30
|
+
extract_chapter_number(File.basename(file, '.xhtml'))
|
|
31
|
+
end
|
|
30
32
|
raise "No chapter files found in #{@chapters_dir}" if nums.empty?
|
|
31
33
|
|
|
32
34
|
nums.sort.uniq
|
|
33
35
|
end
|
|
34
36
|
|
|
37
|
+
def extract_chapter_number(basename)
|
|
38
|
+
if (m = basename.match(/_(\d+)(?:_5|[a-z]+)\z/))
|
|
39
|
+
m[1].to_i + 0.5
|
|
40
|
+
elsif (m = basename.match(/_(\d+)\z/))
|
|
41
|
+
m[1].to_i
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
35
45
|
def check_sequence_completeness(sorted)
|
|
36
|
-
|
|
46
|
+
integers = sorted.select { |n| n == n.to_i }.map(&:to_i)
|
|
47
|
+
return if integers.empty? # e.g. appending only side chapters
|
|
48
|
+
|
|
49
|
+
missing = (integers.first..integers.last).to_a - integers
|
|
37
50
|
raise "Missing chapter numbers: #{missing.join(' ')}" if missing.any?
|
|
38
51
|
end
|
|
39
52
|
end
|
data/lib/epub_tools/cli.rb
CHANGED
|
@@ -1,32 +1,131 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require_relative '
|
|
5
|
-
require_relative '
|
|
3
|
+
require 'optparse'
|
|
4
|
+
require_relative 'version'
|
|
5
|
+
require_relative 'epub_initializer'
|
|
6
|
+
require_relative 'xhtml_extractor'
|
|
7
|
+
require_relative 'split_chapters'
|
|
8
|
+
require_relative 'pdf_converter'
|
|
9
|
+
require_relative 'add_chapters'
|
|
10
|
+
require_relative 'pack_ebook'
|
|
11
|
+
require_relative 'unpack_ebook'
|
|
12
|
+
require_relative 'set_cover'
|
|
13
|
+
require_relative 'compile_book'
|
|
14
|
+
require_relative 'append_book'
|
|
6
15
|
|
|
7
16
|
module EpubTools
|
|
8
|
-
#
|
|
17
|
+
# Command line interface: parses a command's flags and runs the class that implements it
|
|
9
18
|
module CLI
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
TITLE = ['-t TITLE', '--title TITLE', 'Book title', :title, :required].freeze
|
|
20
|
+
AUTHOR = ['-a AUTHOR', '--author AUTHOR', 'Author name', :author, :required].freeze
|
|
21
|
+
COVER = ['-c PATH', '--cover PATH', 'Cover image file path (optional)', :cover_image].freeze
|
|
22
|
+
BOOK_TITLE = ['-t TITLE', '--title TITLE', 'Book title for HTML <title> tags', :book_title, :required].freeze
|
|
23
|
+
CHAPTERS_OUT = ['-o DIR', '--output-dir DIR', 'Output directory for chapter files (default: ./chapters)',
|
|
24
|
+
:output_dir].freeze
|
|
25
|
+
|
|
26
|
+
# Each command's class, usage summary and flags as [short, long, description, option key, :required].
|
|
27
|
+
# Commands log progress unless run with -q, except those marked <tt>verbose: false</tt>.
|
|
28
|
+
COMMANDS = {
|
|
29
|
+
'init' => { class: EpubInitializer, summary: 'Initialize a bare-bones EPUB',
|
|
30
|
+
flags: [TITLE, AUTHOR, ['-o DIR', '--output-dir DIR', 'Destination EPUB directory',
|
|
31
|
+
:destination, :required], COVER] },
|
|
32
|
+
'extract' => { class: XHTMLExtractor, summary: 'Extract XHTML files from EPUBs',
|
|
33
|
+
flags: [['-s DIR', '--source-dir DIR', 'Directory with EPUBs to extract XHTMLs from',
|
|
34
|
+
:source_dir, :required],
|
|
35
|
+
['-t DIR', '--target-dir DIR',
|
|
36
|
+
'Directory where the XHTML files will be extracted to', :target_dir, :required]] },
|
|
37
|
+
'split' => { class: SplitChapters, summary: 'Split XHTML into separate XHTMLs per chapter',
|
|
38
|
+
flags: [['-i FILE', '--input FILE', 'Source XHTML file', :input_file, :required], BOOK_TITLE,
|
|
39
|
+
CHAPTERS_OUT] },
|
|
40
|
+
'pdf' => { class: PDFConverter, summary: 'Convert chapter PDFs into chapter XHTMLs',
|
|
41
|
+
flags: [['-s DIR', '--source-dir DIR', "Dir of PDFs named '12a Title.pdf'", :source_dir, :required],
|
|
42
|
+
BOOK_TITLE, CHAPTERS_OUT] },
|
|
43
|
+
'add' => { class: AddChapters, summary: 'Add chapter XHTML files into an EPUB', verbose: false,
|
|
44
|
+
flags: [['-c DIR', '--chapters-dir DIR', 'Chapters directory', :chapters_dir, :required],
|
|
45
|
+
['-e DIR', '--oebps-dir DIR', 'EPUB OEBPS directory', :oebps_dir, :required]] },
|
|
46
|
+
'pack' => { class: PackEbook, summary: 'Package an EPUB directory into a .epub file',
|
|
47
|
+
flags: [['-i DIR', '--input-dir DIR', 'EPUB directory to package', :input_dir, :required],
|
|
48
|
+
['-o FILE', '--output-file FILE', 'Output EPUB file path', :output_file, :required]] },
|
|
49
|
+
'unpack' => { class: UnpackEbook, summary: 'Unpack an EPUB file into a directory',
|
|
50
|
+
flags: [['-i FILE', '--input-file FILE', 'EPUB file to unpack', :epub_file, :required],
|
|
51
|
+
['-o DIR', '--output-dir DIR',
|
|
52
|
+
'Output directory to extract into (default: basename of epub)', :output_dir]] },
|
|
53
|
+
'cover' => { class: SetCover, summary: 'Add or replace the cover of an EPUB',
|
|
54
|
+
flags: [['-i FILE', '--input-file FILE', 'EPUB to add the cover to', :epub_file, :required],
|
|
55
|
+
['-c PATH', '--cover PATH', 'Cover image: jpg, png, gif or svg', :cover_image, :required]] },
|
|
56
|
+
'compile' => { class: CompileBook,
|
|
57
|
+
summary: 'Takes EPUBs and/or chapter PDFs in a dir and compiles them into a single EPUB.',
|
|
58
|
+
flags: [TITLE, AUTHOR,
|
|
59
|
+
['-s DIR', '--source-dir DIR', 'Directory with EPUBs and/or chapter PDFs',
|
|
60
|
+
:source_dir, :required],
|
|
61
|
+
['-o FILE', '--output FILE', 'EPUB to create (default: <Book_Title>.epub)',
|
|
62
|
+
:output_file], COVER] },
|
|
63
|
+
'append' => { class: AppendBook,
|
|
64
|
+
summary: 'Takes EPUBs and/or chapter PDFs in a dir and appends them to an existing EPUB.',
|
|
65
|
+
flags: [['-s DIR', '--source-dir DIR',
|
|
66
|
+
'Directory with EPUBs and/or chapter PDFs to append', :source_dir, :required],
|
|
67
|
+
['-t FILE', '--target-epub FILE', 'Existing EPUB file to append to',
|
|
68
|
+
:target_epub, :required]] }
|
|
69
|
+
}.freeze
|
|
70
|
+
|
|
71
|
+
module_function
|
|
72
|
+
|
|
73
|
+
# Runs the command named by the first argument, or prints the version or usage
|
|
74
|
+
# @param args [Array<String>] Command line arguments
|
|
75
|
+
# @return [Object] The command instance that ran
|
|
76
|
+
def run(args)
|
|
77
|
+
if ['-v', '--version'].include?(args[0])
|
|
78
|
+
puts VERSION
|
|
79
|
+
exit
|
|
80
|
+
end
|
|
81
|
+
usage unless COMMANDS.key?(args[0])
|
|
82
|
+
|
|
83
|
+
name, *rest = args
|
|
84
|
+
COMMANDS[name][:class].new(parse(name, rest)).tap(&:run)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @param name [String] Command name
|
|
88
|
+
# @param args [Array<String>] The command's arguments
|
|
89
|
+
# @return [Hash] Options for the command's class; exits with the help text if they are invalid
|
|
90
|
+
def parse(name, args)
|
|
91
|
+
command = COMMANDS.fetch(name)
|
|
92
|
+
options = verbose?(command) ? { verbose: true } : {}
|
|
93
|
+
parser = parser_for(name, command, options)
|
|
94
|
+
parser.parse(args)
|
|
95
|
+
missing = missing_flags(command, options)
|
|
96
|
+
abort "Missing required options: #{missing.join(', ')}\n#{parser}" if missing.any?
|
|
97
|
+
|
|
98
|
+
options
|
|
99
|
+
rescue OptionParser::ParseError => e
|
|
100
|
+
abort "#{e.message}\n#{parser}"
|
|
17
101
|
end
|
|
18
102
|
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
registry.register('add', EpubTools::AddChapters, %i[chapters_dir oebps_dir])
|
|
23
|
-
registry.register('extract', EpubTools::XHTMLExtractor, %i[source_dir target_dir], { verbose: true })
|
|
24
|
-
registry.register('split', EpubTools::SplitChapters, %i[input_file book_title],
|
|
25
|
-
{ output_dir: './chapters', prefix: 'chapter', verbose: true })
|
|
26
|
-
registry.register('init', EpubTools::EpubInitializer, %i[title author destination], { verbose: true })
|
|
27
|
-
registry.register('pack', EpubTools::PackEbook, %i[input_dir output_file], { verbose: true })
|
|
28
|
-
registry.register('unpack', EpubTools::UnpackEbook, [:epub_file], { verbose: true })
|
|
29
|
-
registry.register('compile', EpubTools::CompileBook, %i[title author source_dir], { verbose: true })
|
|
103
|
+
# @return [Array<String>] The long names of required flags that were not given, e.g. +--title+
|
|
104
|
+
def missing_flags(command, options)
|
|
105
|
+
command[:flags].filter_map { |_, long, _, key, required| long.split.first if required && options[key].nil? }
|
|
30
106
|
end
|
|
107
|
+
|
|
108
|
+
def parser_for(name, command, options)
|
|
109
|
+
OptionParser.new("Usage: #{program_name} #{name} [options]") do |parser|
|
|
110
|
+
command[:flags].each do |short, long, desc, key, required|
|
|
111
|
+
parser.on(short, long, required ? "#{desc} (required)" : desc) { |v| options[key] = v }
|
|
112
|
+
end
|
|
113
|
+
parser.on('-q', '--quiet', 'Run quietly (default: verbose)') { options[:verbose] = false } if verbose?(command)
|
|
114
|
+
parser.on('-h', '--help', 'Print this help') do
|
|
115
|
+
puts parser
|
|
116
|
+
exit
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def usage
|
|
122
|
+
puts "Usage: #{program_name} COMMAND [options]", 'Commands:'
|
|
123
|
+
COMMANDS.each { |name, command| puts " #{name.ljust(9)} #{command[:summary]}" }
|
|
124
|
+
exit 1
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def verbose?(command) = command.fetch(:verbose, true)
|
|
128
|
+
|
|
129
|
+
def program_name = File.basename($PROGRAM_NAME)
|
|
31
130
|
end
|
|
32
131
|
end
|
|
@@ -1,150 +1,36 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
1
|
# frozen_string_literal: true
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
require_relative 'loggable'
|
|
6
|
-
require_relative 'xhtml_extractor'
|
|
7
|
-
require_relative 'split_chapters'
|
|
3
|
+
require_relative 'book_builder'
|
|
8
4
|
require_relative 'epub_initializer'
|
|
9
|
-
require_relative 'add_chapters'
|
|
10
|
-
require_relative 'pack_ebook'
|
|
11
|
-
require_relative 'compile_workspace'
|
|
12
|
-
require_relative 'chapter_validator'
|
|
13
5
|
|
|
14
6
|
module EpubTools
|
|
15
|
-
#
|
|
16
|
-
class CompileBook
|
|
17
|
-
|
|
7
|
+
# Compiles a new EPUB from source EPUBs by extracting, splitting, and repackaging
|
|
8
|
+
class CompileBook < BookBuilder
|
|
9
|
+
attr_reader :title, :author, :cover_image, :output_file
|
|
18
10
|
|
|
19
|
-
# Book title
|
|
20
|
-
attr_reader :title
|
|
21
|
-
# Book author
|
|
22
|
-
attr_reader :author
|
|
23
|
-
# Path of the input epubs
|
|
24
|
-
attr_reader :source_dir
|
|
25
|
-
# Optional path to the cover image
|
|
26
|
-
attr_reader :cover_image
|
|
27
|
-
# Filename for the final epub
|
|
28
|
-
attr_reader :output_file
|
|
29
|
-
# Optional working directory for intermediate files
|
|
30
|
-
attr_reader :build_dir
|
|
31
|
-
# Whether to print progress to STDOUT
|
|
32
|
-
attr_reader :verbose
|
|
33
|
-
|
|
34
|
-
# Initializes the class
|
|
35
|
-
# @param options [Hash] Configuration options
|
|
36
|
-
# @option options [String] :title Book title (required)
|
|
37
|
-
# @option options [String] :author Book author (required)
|
|
38
|
-
# @option options [String] :source_dir Path of the input epubs (required)
|
|
39
|
-
# @option options [String] :cover_image Optional path to the cover image
|
|
40
|
-
# @option options [String] :output_file Filename for the final epub (default: [title].epub)
|
|
41
|
-
# @option options [String] :build_dir Optional working directory for intermediate files
|
|
42
|
-
# @option options [Boolean] :verbose Whether to print progress to STDOUT (default: false)
|
|
43
11
|
def initialize(options = {})
|
|
12
|
+
super
|
|
44
13
|
@title = options.fetch(:title)
|
|
45
14
|
@author = options.fetch(:author)
|
|
46
|
-
@source_dir = options.fetch(:source_dir)
|
|
47
15
|
@cover_image = options[:cover_image]
|
|
48
16
|
@output_file = options[:output_file] || default_output_file
|
|
49
|
-
@build_dir = options[:build_dir] || File.join(Dir.pwd, '.epub_tools_build')
|
|
50
|
-
@verbose = options[:verbose] || false
|
|
51
|
-
@workspace = CompileWorkspace.new(@build_dir)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Run the full compile workflow
|
|
55
|
-
def run
|
|
56
|
-
setup_workspace
|
|
57
|
-
extract_xhtmls
|
|
58
|
-
split_xhtmls
|
|
59
|
-
validate_chapters
|
|
60
|
-
initialize_epub
|
|
61
|
-
add_chapters
|
|
62
|
-
pack_epub
|
|
63
|
-
finalize_and_cleanup
|
|
64
17
|
end
|
|
65
18
|
|
|
66
19
|
private
|
|
67
20
|
|
|
68
|
-
def
|
|
69
|
-
@workspace.clean
|
|
70
|
-
log "Cleaning build directory #{@build_dir}..."
|
|
71
|
-
@workspace.prepare_directories
|
|
72
|
-
log 'Preparing build directories...'
|
|
73
|
-
end
|
|
21
|
+
def book_title = @title
|
|
74
22
|
|
|
75
|
-
def
|
|
76
|
-
log "Done. Output EPUB: #{File.expand_path(output_file)}"
|
|
77
|
-
@workspace.clean
|
|
78
|
-
output_file
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def default_output_file
|
|
82
|
-
"#{title.gsub(' ', '_')}.epub"
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def extract_xhtmls
|
|
86
|
-
log "Extracting XHTML files from epubs in '#{source_dir}'..."
|
|
87
|
-
XHTMLExtractor.new({
|
|
88
|
-
source_dir: source_dir,
|
|
89
|
-
target_dir: @workspace.xhtml_dir,
|
|
90
|
-
verbose: verbose
|
|
91
|
-
}).run
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def split_xhtmls
|
|
95
|
-
log 'Splitting XHTML files into chapters...'
|
|
96
|
-
Dir.glob(File.join(@workspace.xhtml_dir, '*.xhtml')).each do |xhtml_file|
|
|
97
|
-
split_xhtml_file(xhtml_file)
|
|
98
|
-
end
|
|
99
|
-
end
|
|
23
|
+
def output_path = @output_file
|
|
100
24
|
|
|
101
|
-
def
|
|
102
|
-
base = File.basename(xhtml_file, '.xhtml')
|
|
103
|
-
log "Splitting '#{base}'..."
|
|
104
|
-
SplitChapters.new(build_split_options(xhtml_file)).run
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def build_split_options(xhtml_file)
|
|
108
|
-
{
|
|
109
|
-
input_file: xhtml_file,
|
|
110
|
-
book_title: title,
|
|
111
|
-
output_dir: @workspace.chapters_dir,
|
|
112
|
-
output_prefix: 'chapter',
|
|
113
|
-
verbose: verbose
|
|
114
|
-
}
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def validate_chapters
|
|
118
|
-
ChapterValidator.new(chapters_dir: @workspace.chapters_dir, verbose: verbose).validate
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def initialize_epub
|
|
25
|
+
def before_add_chapters
|
|
122
26
|
log 'Initializing new EPUB...'
|
|
123
|
-
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def build_epub_options
|
|
127
|
-
options = { title: title, author: author, destination: @workspace.epub_dir }
|
|
27
|
+
options = { title: title, author: author, destination: epub_dir }
|
|
128
28
|
options[:cover_image] = cover_image if cover_image
|
|
129
|
-
options
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def add_chapters
|
|
133
|
-
log 'Adding chapters to EPUB...'
|
|
134
|
-
AddChapters.new({
|
|
135
|
-
chapters_dir: @workspace.chapters_dir,
|
|
136
|
-
epub_dir: File.join(@workspace.epub_dir, 'OEBPS'),
|
|
137
|
-
verbose: verbose
|
|
138
|
-
}).run
|
|
29
|
+
EpubInitializer.new(options).run
|
|
139
30
|
end
|
|
140
31
|
|
|
141
|
-
def
|
|
142
|
-
|
|
143
|
-
PackEbook.new({
|
|
144
|
-
input_dir: @workspace.epub_dir,
|
|
145
|
-
output_file: output_file,
|
|
146
|
-
verbose: verbose
|
|
147
|
-
}).run
|
|
32
|
+
def default_output_file
|
|
33
|
+
"#{title.gsub(' ', '_')}.epub"
|
|
148
34
|
end
|
|
149
35
|
end
|
|
150
36
|
end
|