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
|
@@ -37,6 +37,39 @@ class ChapterValidatorTest < Minitest::Test
|
|
|
37
37
|
assert_silent { @validator.validate }
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
def test_validates_sequence_with_half_chapters
|
|
41
|
+
create_chapter_files([1, 2, 3])
|
|
42
|
+
create_half_chapter_files([2])
|
|
43
|
+
|
|
44
|
+
assert_silent { @validator.validate }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_validates_sequence_without_half_chapters_present
|
|
48
|
+
create_chapter_files([1, 2, 3])
|
|
49
|
+
|
|
50
|
+
assert_silent { @validator.validate }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_raises_on_missing_integer_despite_half_chapter
|
|
54
|
+
create_chapter_files([1, 3]) # Missing 2
|
|
55
|
+
create_half_chapter_files([1])
|
|
56
|
+
|
|
57
|
+
error = assert_raises(RuntimeError) { @validator.validate }
|
|
58
|
+
assert_match(/Missing chapter numbers: 2/, error.message)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def test_lettered_chapters_are_not_required
|
|
62
|
+
create_chapter_files(%w[1 1a 1b 2])
|
|
63
|
+
|
|
64
|
+
assert_silent { @validator.validate }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def test_validates_only_side_chapters
|
|
68
|
+
create_chapter_files(%w[7a 7b])
|
|
69
|
+
|
|
70
|
+
assert_silent { @validator.validate }
|
|
71
|
+
end
|
|
72
|
+
|
|
40
73
|
private
|
|
41
74
|
|
|
42
75
|
def create_chapter_files(numbers)
|
|
@@ -44,4 +77,10 @@ class ChapterValidatorTest < Minitest::Test
|
|
|
44
77
|
File.write(File.join(@tmp, "chapter_#{num}.xhtml"), '<html></html>')
|
|
45
78
|
end
|
|
46
79
|
end
|
|
80
|
+
|
|
81
|
+
def create_half_chapter_files(numbers)
|
|
82
|
+
numbers.each do |num|
|
|
83
|
+
File.write(File.join(@tmp, "chapter_#{num}_5.xhtml"), '<html></html>')
|
|
84
|
+
end
|
|
85
|
+
end
|
|
47
86
|
end
|
data/test/cli_commands_test.rb
CHANGED
|
@@ -22,6 +22,7 @@ class CLICommandsTest < Minitest::Test
|
|
|
22
22
|
assert_includes output, 'pack'
|
|
23
23
|
assert_includes output, 'unpack'
|
|
24
24
|
assert_includes output, 'compile'
|
|
25
|
+
assert_includes output, 'append'
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
def test_show_version
|
|
@@ -92,6 +93,14 @@ class CLICommandsTest < Minitest::Test
|
|
|
92
93
|
assert_includes output, '--output-file FILE'
|
|
93
94
|
end
|
|
94
95
|
|
|
96
|
+
def test_append_command
|
|
97
|
+
output = `#{@bin_path} append --help`
|
|
98
|
+
|
|
99
|
+
assert_match(/Usage: epub-tools append \[options\]/, output)
|
|
100
|
+
assert_includes output, '--source-dir DIR'
|
|
101
|
+
assert_includes output, '--target-epub FILE'
|
|
102
|
+
end
|
|
103
|
+
|
|
95
104
|
def test_unpack_command
|
|
96
105
|
output = `#{@bin_path} unpack --help`
|
|
97
106
|
|
data/test/cli_test.rb
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'test_helper'
|
|
4
|
-
require_relative '
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
require_relative '../lib/epub_tools'
|
|
5
|
+
|
|
6
|
+
class CLITest < Minitest::Test
|
|
7
|
+
def test_parse_maps_flags_to_option_keys
|
|
8
|
+
options = EpubTools::CLI.parse('init', %w[-t Title -a Author -o dir -c cover.jpg])
|
|
9
|
+
|
|
10
|
+
assert_equal({ verbose: true, title: 'Title', author: 'Author', destination: 'dir', cover_image: 'cover.jpg' },
|
|
11
|
+
options)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def test_quiet_turns_off_verbose
|
|
15
|
+
refute EpubTools::CLI.parse('pack', %w[-i dir -o out.epub -q])[:verbose]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_commands_without_verbose_output_have_no_quiet_flag
|
|
19
|
+
assert_equal({ chapters_dir: 'c', oebps_dir: 'e' }, EpubTools::CLI.parse('add', %w[-c c -e e]))
|
|
20
|
+
assert_output('', /invalid option: -q/) do
|
|
21
|
+
assert_raises(SystemExit) { EpubTools::CLI.parse('add', %w[-c c -e e -q]) }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_missing_required_options_name_their_flags
|
|
26
|
+
assert_output('', /Missing required options: --output-dir\n/) do
|
|
27
|
+
assert_raises(SystemExit) { EpubTools::CLI.parse('init', %w[-t Title -a Author]) }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_run_builds_and_runs_the_command
|
|
32
|
+
Dir.mktmpdir do |dir|
|
|
33
|
+
File.write(File.join(dir, 'mimetype'), 'application/epub+zip')
|
|
34
|
+
out = File.join(dir, '..', "#{File.basename(dir)}.epub")
|
|
35
|
+
command = EpubTools::CLI.run(['pack', '-i', dir, '-o', out, '-q'])
|
|
36
|
+
|
|
37
|
+
assert_instance_of EpubTools::PackEbook, command
|
|
38
|
+
assert_path_exists out
|
|
39
|
+
ensure
|
|
40
|
+
FileUtils.rm_f(out)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/test/compile_book_test.rb
CHANGED
|
@@ -101,11 +101,10 @@ class CompileBookTest < Minitest::Test
|
|
|
101
101
|
output_file: 'test.epub'
|
|
102
102
|
)
|
|
103
103
|
|
|
104
|
-
# Mock the workflow methods to avoid complex file setup
|
|
105
104
|
def cb.extract_xhtmls; end
|
|
106
105
|
def cb.split_xhtmls; end
|
|
107
106
|
def cb.validate_chapters; end
|
|
108
|
-
def cb.
|
|
107
|
+
def cb.before_add_chapters; end
|
|
109
108
|
def cb.add_chapters; end
|
|
110
109
|
def cb.pack_epub; end
|
|
111
110
|
|
|
@@ -114,4 +113,18 @@ class CompileBookTest < Minitest::Test
|
|
|
114
113
|
|
|
115
114
|
assert_equal 'test.epub', result
|
|
116
115
|
end
|
|
116
|
+
|
|
117
|
+
def test_relative_output_is_written_to_working_directory
|
|
118
|
+
book = File.join(@tmp, 'part')
|
|
119
|
+
FileUtils.mkdir_p(File.join(book, 'OEBPS'))
|
|
120
|
+
File.write(File.join(book, 'mimetype'), 'application/epub+zip')
|
|
121
|
+
File.write(File.join(book, 'OEBPS', 'text.xhtml'), '<html><body><p>Chapter 1</p><p>Text</p></body></html>')
|
|
122
|
+
EpubTools::PackEbook.new(input_dir: book, output_file: File.join(@source, 'part.epub')).run
|
|
123
|
+
|
|
124
|
+
Dir.chdir(@tmp) do
|
|
125
|
+
EpubTools::CompileBook.new(title: @title, author: @author, source_dir: @source, output_file: 'out.epub').run
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
assert_path_exists File.join(@tmp, 'out.epub')
|
|
129
|
+
end
|
|
117
130
|
end
|
|
@@ -24,6 +24,22 @@ class EpubInitializerTest < Minitest::Test
|
|
|
24
24
|
verify_file_contents
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
def test_style_comes_from_the_gem_not_the_working_directory
|
|
28
|
+
Dir.chdir(@tmp) { EpubTools::EpubInitializer.new(title: @title, author: @author, destination: @dest).run }
|
|
29
|
+
|
|
30
|
+
assert_equal File.read(File.expand_path('../style.css', __dir__)), File.read(File.join(@dest, 'OEBPS', 'style.css'))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_title_and_author_are_escaped
|
|
34
|
+
EpubTools::EpubInitializer.new(title: 'Tom & Jerry', author: 'A <B>', destination: @dest).run
|
|
35
|
+
opf = strict_xml('package.opf')
|
|
36
|
+
dc = { 'dc' => 'http://purl.org/dc/elements/1.1/' }
|
|
37
|
+
|
|
38
|
+
assert_equal 'Tom & Jerry', opf.at_xpath('//dc:title', dc).text
|
|
39
|
+
assert_equal 'A <B>', opf.at_xpath('//dc:creator', dc).text
|
|
40
|
+
assert_equal 'by A <B>', strict_xml('title.xhtml').at_xpath('//xmlns:p[@class="author"]').text
|
|
41
|
+
end
|
|
42
|
+
|
|
27
43
|
def test_run_with_cover_image
|
|
28
44
|
# create dummy image
|
|
29
45
|
cover = File.join(@tmp, 'cover.png')
|
|
@@ -117,6 +133,8 @@ class EpubInitializerTest < Minitest::Test
|
|
|
117
133
|
|
|
118
134
|
private
|
|
119
135
|
|
|
136
|
+
def strict_xml(name) = Nokogiri::XML(File.read(File.join(@dest, 'OEBPS', name)), &:strict)
|
|
137
|
+
|
|
120
138
|
def verify_directory_structure
|
|
121
139
|
assert Dir.exist?(@dest)
|
|
122
140
|
assert File.directory?(File.join(@dest, 'META-INF'))
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'test_helper'
|
|
4
|
+
require_relative '../lib/epub_tools/pdf_converter'
|
|
5
|
+
|
|
6
|
+
class PDFConverterTest < Minitest::Test
|
|
7
|
+
XML = <<~XML
|
|
8
|
+
<pdf2xml><page number="1">
|
|
9
|
+
<fontspec id="0" size="42"/><fontspec id="1" size="18"/>
|
|
10
|
+
<text top="105" left="231" width="400" font="0">Heading typed in the PDF</text>
|
|
11
|
+
<text top="198" left="135" width="400" font="1">Tom & Jerry’s text.</text>
|
|
12
|
+
<text top="235" left="135" width="400" font="1">More text.</text>
|
|
13
|
+
</page></pdf2xml>
|
|
14
|
+
XML
|
|
15
|
+
|
|
16
|
+
def setup
|
|
17
|
+
@tmp = Dir.mktmpdir
|
|
18
|
+
@source = File.join(@tmp, 'pdfs')
|
|
19
|
+
@output = File.join(@tmp, 'chapters')
|
|
20
|
+
FileUtils.mkdir_p(@source)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def teardown
|
|
24
|
+
FileUtils.rm_rf(@tmp)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_names_chapters_after_their_pdfs
|
|
28
|
+
['0 Cast List.pdf', "12a Don't Stop.pdf"].each { |name| FileUtils.touch(File.join(@source, name)) }
|
|
29
|
+
|
|
30
|
+
paths = converter.run
|
|
31
|
+
|
|
32
|
+
assert_equal %w[chapter_0.xhtml chapter_12a.xhtml], paths.map { |path| File.basename(path) }.sort
|
|
33
|
+
assert_equal 'Cast List', heading('chapter_0.xhtml')
|
|
34
|
+
assert_equal 'Chapter 12a: Don’t Stop', heading('chapter_12a.xhtml')
|
|
35
|
+
assert_includes File.read(File.join(@output, 'chapter_12a.xhtml')), '<p>Tom & Jerry’s text.</p>'
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_names_the_epilogue
|
|
39
|
+
FileUtils.touch(File.join(@source, 'Epilogue One Month.pdf'))
|
|
40
|
+
|
|
41
|
+
assert_equal(['chapter_epilogue.xhtml'], converter.run.map { |path| File.basename(path) })
|
|
42
|
+
assert_equal 'Epilogue: One Month', heading('chapter_epilogue.xhtml')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_rejects_pdfs_without_a_chapter_number
|
|
46
|
+
FileUtils.touch(File.join(@source, 'Afterword.pdf'))
|
|
47
|
+
|
|
48
|
+
error = assert_raises(ArgumentError) { converter.run }
|
|
49
|
+
assert_match(/Afterword\.pdf/, error.message)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def test_does_nothing_without_pdfs
|
|
53
|
+
assert_empty converter.run
|
|
54
|
+
refute_path_exists @output
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def converter
|
|
60
|
+
xml = XML
|
|
61
|
+
EpubTools::PDFConverter.new(source_dir: @source, output_dir: @output, book_title: 'Book').tap do |converter|
|
|
62
|
+
converter.define_singleton_method(:pdftohtml) { |_pdf| Nokogiri::XML(xml) }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def heading(file)
|
|
67
|
+
Nokogiri::XML(File.read(File.join(@output, file))).at_xpath('//xmlns:h1').text
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'test_helper'
|
|
4
|
+
require_relative '../lib/epub_tools/pdf_layout'
|
|
5
|
+
|
|
6
|
+
class PDFLayoutTest < Minitest::Test
|
|
7
|
+
# Shaped like `pdftohtml -xml` output: body text at left=108, first-line indents at 135, lines 25px apart
|
|
8
|
+
# and paragraphs 37px apart, with full lines ending at 800.
|
|
9
|
+
XML = <<~XML
|
|
10
|
+
<pdf2xml>
|
|
11
|
+
<page number="1">
|
|
12
|
+
<fontspec id="0" size="42"/><fontspec id="1" size="18"/>
|
|
13
|
+
<text top="105" left="231" width="400" font="0">Chapter 1: Title in the PDF</text>
|
|
14
|
+
<text top="198" left="135" width="200" font="1"><i><b>Monday, 9:00 am</b></i></text>
|
|
15
|
+
<text top="231" left="135" width="600" font="1">The first paragraph has <i>italic words that </i></text>
|
|
16
|
+
<text top="256" left="108" width="692" font="1"><i>continue</i> on this line and ends with a half-</text>
|
|
17
|
+
<text top="281" left="108" width="692" font="1">dressed word and no trailing space</text>
|
|
18
|
+
<text top="306" left="108" width="600" font="1">before this line, which is full </text>
|
|
19
|
+
<text top="306" left="708" width="92" font="1">width </text>
|
|
20
|
+
</page>
|
|
21
|
+
<page number="2">
|
|
22
|
+
<text top="107" left="108" width="300" font="1">so it continues after the page break.</text>
|
|
23
|
+
<text top="144" left="108" width="400" font="1">A block paragraph after a gap.</text>
|
|
24
|
+
<text top="181" left="460" width="28" font="1">*** </text>
|
|
25
|
+
<text top="218" left="135" width="665" font="1">After the break, a paragraph whose writer hit</text>
|
|
26
|
+
<text top="255" left="135" width="200" font="1">enter by mistake.</text>
|
|
27
|
+
<text top="292" left="135" width="100" font="1">Short end.</text>
|
|
28
|
+
</page>
|
|
29
|
+
<page number="3">
|
|
30
|
+
<text top="107" left="108" width="400" font="1">A new paragraph, as the last page ended short.</text>
|
|
31
|
+
<text top="144" left="126" width="300" font="1">• A bullet that wraps</text>
|
|
32
|
+
<text top="164" left="138" width="200" font="1">onto a second line</text>
|
|
33
|
+
<text top="184" left="126" width="200" font="1">• Second bullet</text>
|
|
34
|
+
</page>
|
|
35
|
+
</pdf2xml>
|
|
36
|
+
XML
|
|
37
|
+
|
|
38
|
+
def test_rebuilds_paragraphs
|
|
39
|
+
html = EpubTools::PDFLayout.new(Nokogiri::XML(XML)).to_html
|
|
40
|
+
|
|
41
|
+
assert_equal [
|
|
42
|
+
'<p><i><b>Monday, 9:00 am</b></i></p>',
|
|
43
|
+
'<p>The first paragraph has <i>italic words that continue</i> on this line and ends with a half-dressed ' \
|
|
44
|
+
'word and no trailing space before this line, which is full width so it continues after the page break.</p>',
|
|
45
|
+
'<p>A block paragraph after a gap.</p>',
|
|
46
|
+
'<hr/>',
|
|
47
|
+
'<p>After the break, a paragraph whose writer hit enter by mistake.</p>',
|
|
48
|
+
'<p>Short end.</p>',
|
|
49
|
+
'<p>A new paragraph, as the last page ended short.</p>',
|
|
50
|
+
'<ul><li>A bullet that wraps onto a second line</li><li>Second bullet</li></ul>'
|
|
51
|
+
], html.split("\n ")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_empty_without_body_text
|
|
55
|
+
assert_predicate EpubTools::PDFLayout.new(Nokogiri::XML('<pdf2xml><page number="1"/></pdf2xml>')), :empty?
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'test_helper'
|
|
4
|
+
require_relative '../lib/epub_tools/set_cover'
|
|
5
|
+
require_relative '../lib/epub_tools/epub_initializer'
|
|
6
|
+
require 'zip'
|
|
7
|
+
|
|
8
|
+
class SetCoverTest < Minitest::Test
|
|
9
|
+
DC = { 'dc' => 'http://purl.org/dc/elements/1.1/' }.freeze
|
|
10
|
+
|
|
11
|
+
def setup
|
|
12
|
+
@tmp = Dir.mktmpdir
|
|
13
|
+
@epub = File.join(@tmp, 'book.epub')
|
|
14
|
+
book = EpubTools::EpubInitializer.new(title: 'Book', author: 'Me', destination: File.join(@tmp, 'book')).run
|
|
15
|
+
EpubTools::PackEbook.new(input_dir: book, output_file: @epub).run
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def teardown
|
|
19
|
+
FileUtils.rm_rf(@tmp)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_adds_then_replaces_the_cover_keeping_the_book_identity
|
|
23
|
+
identifier = book_identifier
|
|
24
|
+
apply_cover('art.png')
|
|
25
|
+
apply_cover('art.jpg')
|
|
26
|
+
|
|
27
|
+
assert_cover_entries('cover.jpg', 'image/jpeg')
|
|
28
|
+
assert_cover_files('cover.jpg', replaced: 'cover.png')
|
|
29
|
+
assert_equal identifier, book_identifier
|
|
30
|
+
assert_path_exists "#{@epub}.bak"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_rejects_unsupported_image_types
|
|
34
|
+
bmp = File.join(@tmp, 'art.bmp')
|
|
35
|
+
File.write(bmp, 'BMP')
|
|
36
|
+
|
|
37
|
+
error = assert_raises(ArgumentError) { EpubTools::SetCover.new(epub_file: @epub, cover_image: bmp) }
|
|
38
|
+
assert_match(/unsupported cover image type/, error.message)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def apply_cover(name)
|
|
44
|
+
path = File.join(@tmp, name)
|
|
45
|
+
File.write(path, 'IMAGE')
|
|
46
|
+
EpubTools::SetCover.new(epub_file: @epub, cover_image: path).run
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def assert_cover_entries(href, media_type)
|
|
50
|
+
covers = opf.xpath('//xmlns:item[@properties="cover-image"]').map { |item| [item['href'], item['media-type']] }
|
|
51
|
+
spine = opf.xpath('//xmlns:itemref').map { |ref| ref['idref'] }
|
|
52
|
+
|
|
53
|
+
assert_equal [[href, media_type]], covers
|
|
54
|
+
assert_equal %w[cover-page title], spine
|
|
55
|
+
assert_equal 1, opf.xpath('//xmlns:meta[@name="cover"]').size
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def assert_cover_files(image, replaced:)
|
|
59
|
+
assert_includes zip_entry('OEBPS/cover.xhtml'), %(src="#{image}")
|
|
60
|
+
refute_includes Zip::File.open(@epub) { |zip| zip.map(&:name) }, "OEBPS/#{replaced}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def book_identifier = opf.at_xpath('//dc:identifier', DC).text
|
|
64
|
+
|
|
65
|
+
def opf = Nokogiri::XML(zip_entry('OEBPS/package.opf'))
|
|
66
|
+
|
|
67
|
+
def zip_entry(name) = Zip::File.open(@epub) { |zip| zip.read(name) }
|
|
68
|
+
end
|
data/test/split_chapters_test.rb
CHANGED
|
@@ -29,8 +29,7 @@ class SplitChaptersTest < Minitest::Test
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def test_run_generates_chapter_files
|
|
32
|
-
result = EpubTools::SplitChapters.new(input_file: @input, book_title: 'BookTitle', output_dir: @out
|
|
33
|
-
output_prefix: 'chap').run
|
|
32
|
+
result = EpubTools::SplitChapters.new(input_file: @input, book_title: 'BookTitle', output_dir: @out).run
|
|
34
33
|
|
|
35
34
|
verify_return_value(result)
|
|
36
35
|
verify_generated_files_exist(result)
|
|
@@ -46,9 +45,9 @@ class SplitChaptersTest < Minitest::Test
|
|
|
46
45
|
|
|
47
46
|
def verify_generated_files_exist(result)
|
|
48
47
|
expected_paths = [
|
|
49
|
-
File.join(@out, '
|
|
50
|
-
File.join(@out, '
|
|
51
|
-
File.join(@out, '
|
|
48
|
+
File.join(@out, 'chapter_0.xhtml'),
|
|
49
|
+
File.join(@out, 'chapter_1.xhtml'),
|
|
50
|
+
File.join(@out, 'chapter_2.xhtml')
|
|
52
51
|
]
|
|
53
52
|
|
|
54
53
|
expected_paths.each do |path|
|
|
@@ -58,7 +57,7 @@ class SplitChaptersTest < Minitest::Test
|
|
|
58
57
|
|
|
59
58
|
files = Dir.children(@out)
|
|
60
59
|
|
|
61
|
-
['
|
|
60
|
+
['chapter_0.xhtml', 'chapter_1.xhtml', 'chapter_2.xhtml'].each do |file|
|
|
62
61
|
assert_includes files, file
|
|
63
62
|
end
|
|
64
63
|
end
|
|
@@ -70,7 +69,7 @@ class SplitChaptersTest < Minitest::Test
|
|
|
70
69
|
end
|
|
71
70
|
|
|
72
71
|
def verify_prologue_content
|
|
73
|
-
prologue = File.read(File.join(@out, '
|
|
72
|
+
prologue = File.read(File.join(@out, 'chapter_0.xhtml'))
|
|
74
73
|
|
|
75
74
|
assert_includes prologue, '<h1>Prologue</h1>'
|
|
76
75
|
assert_includes prologue, 'Intro text'
|
|
@@ -78,7 +77,7 @@ class SplitChaptersTest < Minitest::Test
|
|
|
78
77
|
end
|
|
79
78
|
|
|
80
79
|
def verify_chapter_one_content
|
|
81
|
-
ch1 = File.read(File.join(@out, '
|
|
80
|
+
ch1 = File.read(File.join(@out, 'chapter_1.xhtml'))
|
|
82
81
|
|
|
83
82
|
assert_includes ch1, '<h1>Chapter 1</h1>'
|
|
84
83
|
assert_includes ch1, 'First paragraph'
|
|
@@ -86,9 +85,85 @@ class SplitChaptersTest < Minitest::Test
|
|
|
86
85
|
end
|
|
87
86
|
|
|
88
87
|
def verify_chapter_two_content
|
|
89
|
-
ch2 = File.read(File.join(@out, '
|
|
88
|
+
ch2 = File.read(File.join(@out, 'chapter_2.xhtml'))
|
|
90
89
|
|
|
91
90
|
assert_includes ch2, '<h1>Chapter 2</h1>'
|
|
92
91
|
assert_includes ch2, 'Second paragraph'
|
|
93
92
|
end
|
|
94
93
|
end
|
|
94
|
+
|
|
95
|
+
class SplitChaptersContinuedTest < Minitest::Test
|
|
96
|
+
def setup
|
|
97
|
+
@tmp = Dir.mktmpdir
|
|
98
|
+
@input = File.join(@tmp, 'input.xhtml')
|
|
99
|
+
@out = File.join(@tmp, 'out')
|
|
100
|
+
content = <<~HTML
|
|
101
|
+
<?xml version="1.0"?>
|
|
102
|
+
<html xmlns="http://www.w3.org/1999/xhtml">
|
|
103
|
+
<body>
|
|
104
|
+
<p>Chapter 1</p>
|
|
105
|
+
<p>First paragraph</p>
|
|
106
|
+
<p>Chapter 1 (Continued)</p>
|
|
107
|
+
<p>Continued text</p>
|
|
108
|
+
<p>Chapter 2</p>
|
|
109
|
+
<p>Second paragraph</p>
|
|
110
|
+
</body>
|
|
111
|
+
</html>
|
|
112
|
+
HTML
|
|
113
|
+
File.write(@input, content)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def teardown
|
|
117
|
+
FileUtils.remove_entry(@tmp)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def test_splits_continued_chapters
|
|
121
|
+
result = EpubTools::SplitChapters.new(
|
|
122
|
+
input_file: @input, book_title: 'Test', output_dir: @out
|
|
123
|
+
).run
|
|
124
|
+
|
|
125
|
+
assert_equal 3, result.size
|
|
126
|
+
assert_includes result, File.join(@out, 'chapter_1.xhtml')
|
|
127
|
+
assert_includes result, File.join(@out, 'chapter_1_5.xhtml')
|
|
128
|
+
assert_includes result, File.join(@out, 'chapter_2.xhtml')
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def test_continued_chapter_content
|
|
132
|
+
EpubTools::SplitChapters.new(
|
|
133
|
+
input_file: @input, book_title: 'Test', output_dir: @out
|
|
134
|
+
).run
|
|
135
|
+
|
|
136
|
+
continued = File.read(File.join(@out, 'chapter_1_5.xhtml'))
|
|
137
|
+
|
|
138
|
+
assert_includes continued, '<h1>Chapter 1.5</h1>'
|
|
139
|
+
assert_includes continued, 'Continued text'
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def test_book_title_is_escaped
|
|
143
|
+
EpubTools::SplitChapters.new(input_file: @input, book_title: 'Tom & Jerry', output_dir: @out).run
|
|
144
|
+
chapter = Nokogiri::XML(File.read(File.join(@out, 'chapter_1.xhtml')), &:strict)
|
|
145
|
+
|
|
146
|
+
assert_equal 'Tom & Jerry - Chapter 1', chapter.at_xpath('//xmlns:title').text
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def test_continued_marker_case_insensitive
|
|
150
|
+
content = <<~HTML
|
|
151
|
+
<?xml version="1.0"?>
|
|
152
|
+
<html xmlns="http://www.w3.org/1999/xhtml">
|
|
153
|
+
<body>
|
|
154
|
+
<p>Chapter 5</p>
|
|
155
|
+
<p>Text</p>
|
|
156
|
+
<p>Chapter 5 (continued)</p>
|
|
157
|
+
<p>More text</p>
|
|
158
|
+
</body>
|
|
159
|
+
</html>
|
|
160
|
+
HTML
|
|
161
|
+
File.write(@input, content)
|
|
162
|
+
|
|
163
|
+
result = EpubTools::SplitChapters.new(
|
|
164
|
+
input_file: @input, book_title: 'Test', output_dir: @out
|
|
165
|
+
).run
|
|
166
|
+
|
|
167
|
+
assert_includes result, File.join(@out, 'chapter_5_5.xhtml')
|
|
168
|
+
end
|
|
169
|
+
end
|
data/test/style_finder_test.rb
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'yaml'
|
|
4
3
|
require_relative 'test_helper'
|
|
5
4
|
require_relative '../lib/epub_tools/style_finder'
|
|
6
5
|
|
|
7
|
-
class
|
|
6
|
+
class StyleFinderTest < Minitest::Test
|
|
8
7
|
def setup
|
|
9
8
|
@tmp = Dir.mktmpdir
|
|
10
9
|
@xhtml = File.join(@tmp, 'doc.xhtml')
|
|
@@ -16,7 +15,6 @@ class StyleFinder < Minitest::Test
|
|
|
16
15
|
</style></head><body></body></html>
|
|
17
16
|
HTML
|
|
18
17
|
File.write(@xhtml, content)
|
|
19
|
-
@yaml = File.join(@tmp, 'classes.yaml')
|
|
20
18
|
end
|
|
21
19
|
|
|
22
20
|
def teardown
|
|
@@ -24,11 +22,7 @@ class StyleFinder < Minitest::Test
|
|
|
24
22
|
end
|
|
25
23
|
|
|
26
24
|
def test_finds_italic_and_bold_classes
|
|
27
|
-
EpubTools::StyleFinder.new(file_path: @xhtml
|
|
28
|
-
data = YAML.load_file(@yaml)
|
|
29
|
-
|
|
30
|
-
assert_equal ['c1'], data['italics']
|
|
31
|
-
assert_equal ['c2'], data['bolds']
|
|
25
|
+
assert_equal({ italics: ['c1'], bolds: ['c2'] }, EpubTools::StyleFinder.new(file_path: @xhtml).run)
|
|
32
26
|
end
|
|
33
27
|
|
|
34
28
|
def test_verbose_mode
|
|
@@ -37,7 +31,7 @@ class StyleFinder < Minitest::Test
|
|
|
37
31
|
Classes with font-weight: 700: c2
|
|
38
32
|
OUTPUT
|
|
39
33
|
assert_output(text) do
|
|
40
|
-
EpubTools::StyleFinder.new(file_path: @xhtml,
|
|
34
|
+
EpubTools::StyleFinder.new(file_path: @xhtml, verbose: true).run
|
|
41
35
|
end
|
|
42
36
|
end
|
|
43
37
|
end
|
data/test/test_helper.rb
CHANGED
data/test/unpack_ebook_test.rb
CHANGED
|
@@ -43,7 +43,7 @@ class UnpackEbookTest < Minitest::Test
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def add_remaining_files_to_zip(zip)
|
|
46
|
-
Dir.glob(File.join(@build_dir, '**', '*')
|
|
46
|
+
Dir.glob(File.join(@build_dir, '**', '*')).each do |src_path|
|
|
47
47
|
add_file_or_directory_to_zip(zip, src_path)
|
|
48
48
|
end
|
|
49
49
|
end
|
data/test/xhtml_cleaner_test.rb
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'yaml'
|
|
4
3
|
require_relative 'test_helper'
|
|
5
4
|
require_relative '../lib/epub_tools/xhtml_cleaner'
|
|
6
5
|
|
|
7
6
|
class XHTMLCleanerTest < Minitest::Test
|
|
8
7
|
def setup
|
|
9
8
|
@tmp = Dir.mktmpdir
|
|
10
|
-
@config = File.join(@tmp, 'config.yaml')
|
|
11
|
-
File.write(@config, { 'italics' => ['itclass'], 'bolds' => ['boldclass'] }.to_yaml)
|
|
12
9
|
@file = File.join(@tmp, 'test.xhtml')
|
|
13
10
|
content = <<~HTML
|
|
14
11
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -30,7 +27,7 @@ class XHTMLCleanerTest < Minitest::Test
|
|
|
30
27
|
end
|
|
31
28
|
|
|
32
29
|
def test_cleaner_removes_and_transforms_tags
|
|
33
|
-
result = EpubTools::XHTMLCleaner.new(filename: @file,
|
|
30
|
+
result = EpubTools::XHTMLCleaner.new(filename: @file, classes: { italics: ['itclass'], bolds: ['boldclass'] }).run
|
|
34
31
|
|
|
35
32
|
# Check return value is the filename that was cleaned
|
|
36
33
|
assert_equal @file, result
|