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 +7 -0
- data/.github/workflows/ci.yml +21 -0
- data/.gitignore +7 -0
- data/.nova/Configuration.json +4 -0
- data/.ruby-version +1 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +53 -0
- data/README.md +118 -0
- data/Rakefile +9 -0
- data/bin/epub-tools +107 -0
- data/epub_tools.gemspec +21 -0
- data/lib/epub_tools/add_chapters_to_epub.rb +87 -0
- data/lib/epub_tools/cli_helper.rb +31 -0
- data/lib/epub_tools/compile_book.rb +121 -0
- data/lib/epub_tools/epub_initializer.rb +197 -0
- data/lib/epub_tools/pack_ebook.rb +60 -0
- data/lib/epub_tools/split_chapters.rb +105 -0
- data/lib/epub_tools/text_style_class_finder.rb +47 -0
- data/lib/epub_tools/unpack_ebook.rb +46 -0
- data/lib/epub_tools/version.rb +3 -0
- data/lib/epub_tools/xhtml_cleaner.rb +75 -0
- data/lib/epub_tools/xhtml_extractor.rb +46 -0
- data/lib/epub_tools.rb +12 -0
- data/style.css +99 -0
- data/test/add_chapters_to_epub_test.rb +92 -0
- data/test/compile_book_test.rb +193 -0
- data/test/epub_initializer_test.rb +55 -0
- data/test/pack_ebook_test.rb +68 -0
- data/test/split_chapters_test.rb +53 -0
- data/test/test_helper.rb +9 -0
- data/test/text_style_class_finder_test.rb +40 -0
- data/test/unpack_ebook_test.rb +58 -0
- data/test/xhtml_cleaner_test.rb +39 -0
- data/test/xhtml_extractor_test.rb +31 -0
- metadata +142 -0
@@ -0,0 +1,92 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
require_relative '../lib/epub_tools/add_chapters_to_epub'
|
3
|
+
require 'nokogiri'
|
4
|
+
|
5
|
+
class AddChaptersToEpubTest < Minitest::Test
|
6
|
+
def setup
|
7
|
+
@tmp = Dir.mktmpdir
|
8
|
+
# Directories for chapters and EPUB OEBPS
|
9
|
+
@chapters_dir = File.join(@tmp, 'chapters')
|
10
|
+
@epub_dir = File.join(@tmp, 'OEBPS')
|
11
|
+
Dir.mkdir(@chapters_dir)
|
12
|
+
Dir.mkdir(@epub_dir)
|
13
|
+
|
14
|
+
# Create sample chapter files
|
15
|
+
@chap0 = File.join(@chapters_dir, 'chapter_0.xhtml')
|
16
|
+
@chap1 = File.join(@chapters_dir, 'chapter_1.xhtml')
|
17
|
+
File.write(@chap0, '<html><body><p>Prologue</p></body></html>')
|
18
|
+
File.write(@chap1, '<html><body><p>First</p></body></html>')
|
19
|
+
|
20
|
+
# Create minimal package.opf
|
21
|
+
@opf_file = File.join(@epub_dir, 'package.opf')
|
22
|
+
opf_content = <<~XML
|
23
|
+
<?xml version="1.0"?>
|
24
|
+
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="pub-id" xml:lang="en">
|
25
|
+
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
|
26
|
+
</metadata>
|
27
|
+
<manifest>
|
28
|
+
</manifest>
|
29
|
+
<spine>
|
30
|
+
</spine>
|
31
|
+
</package>
|
32
|
+
XML
|
33
|
+
File.write(@opf_file, opf_content)
|
34
|
+
|
35
|
+
# Create minimal nav.xhtml
|
36
|
+
@nav_file = File.join(@epub_dir, 'nav.xhtml')
|
37
|
+
nav_content = <<~XHTML
|
38
|
+
<?xml version="1.0" encoding="utf-8"?>
|
39
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" lang="en">
|
40
|
+
<body>
|
41
|
+
<nav epub:type="toc" id="toc">
|
42
|
+
<ol>
|
43
|
+
</ol>
|
44
|
+
</nav>
|
45
|
+
</body>
|
46
|
+
</html>
|
47
|
+
XHTML
|
48
|
+
File.write(@nav_file, nav_content)
|
49
|
+
end
|
50
|
+
|
51
|
+
def teardown
|
52
|
+
FileUtils.remove_entry(@tmp)
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_run_moves_files_and_updates_opf_and_nav
|
56
|
+
# Run the add chapters task
|
57
|
+
EpubTools::AddChaptersToEpub.new(@chapters_dir, @epub_dir).run
|
58
|
+
|
59
|
+
# Original chapter files should be moved
|
60
|
+
assert_empty Dir.glob(File.join(@chapters_dir, '*.xhtml'))
|
61
|
+
assert File.exist?(File.join(@epub_dir, 'chapter_0.xhtml'))
|
62
|
+
assert File.exist?(File.join(@epub_dir, 'chapter_1.xhtml'))
|
63
|
+
|
64
|
+
# package.opf should include manifest items and spine refs
|
65
|
+
doc = Nokogiri::XML(File.read(@opf_file)) { |cfg| cfg.default_xml.noblanks }
|
66
|
+
items = doc.xpath('//xmlns:manifest/xmlns:item')
|
67
|
+
idrefs = doc.xpath('//xmlns:spine/xmlns:itemref')
|
68
|
+
hrefs = items.map { |i| i['href'] }
|
69
|
+
ids = items.map { |i| i['id'] }
|
70
|
+
refs = idrefs.map { |ir| ir['idref'] }
|
71
|
+
|
72
|
+
assert_includes hrefs, 'chapter_0.xhtml'
|
73
|
+
assert_includes hrefs, 'chapter_1.xhtml'
|
74
|
+
assert_includes ids, 'chap0'
|
75
|
+
assert_includes ids, 'chap1'
|
76
|
+
assert_includes refs, 'chap0'
|
77
|
+
assert_includes refs, 'chap1'
|
78
|
+
|
79
|
+
# nav.xhtml should have list entries for each chapter
|
80
|
+
nav_doc = Nokogiri::XML(File.read(@nav_file))
|
81
|
+
# strip namespaces for easy querying
|
82
|
+
nav_doc.remove_namespaces!
|
83
|
+
links = nav_doc.xpath('//nav/ol/li/a')
|
84
|
+
assert_equal 2, links.size
|
85
|
+
# First is Prologue (chapter_0)
|
86
|
+
assert_equal 'chapter_0.xhtml', links[0]['href']
|
87
|
+
assert_equal 'Prologue', links[0].text
|
88
|
+
# Second is Chapter 1
|
89
|
+
assert_equal 'chapter_1.xhtml', links[1]['href']
|
90
|
+
assert_equal 'Chapter 1', links[1].text
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
require_relative '../lib/epub_tools/compile_book'
|
3
|
+
|
4
|
+
class CompileBookTest < Minitest::Test
|
5
|
+
def setup
|
6
|
+
@tmp = Dir.mktmpdir
|
7
|
+
@title = 'My Title'
|
8
|
+
@author = 'Me'
|
9
|
+
@source = File.join(@tmp, 'src')
|
10
|
+
FileUtils.mkdir_p(@source)
|
11
|
+
end
|
12
|
+
|
13
|
+
def teardown
|
14
|
+
FileUtils.remove_entry(@tmp)
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_default_output_file
|
18
|
+
cb = EpubTools::CompileBook.new(
|
19
|
+
title: @title,
|
20
|
+
author: @author,
|
21
|
+
source_dir: @source,
|
22
|
+
build_dir: @tmp
|
23
|
+
)
|
24
|
+
assert_equal 'My_Title.epub', cb.output_file
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_override_output_file
|
28
|
+
cb = EpubTools::CompileBook.new(
|
29
|
+
title: @title,
|
30
|
+
author: @author,
|
31
|
+
source_dir: @source,
|
32
|
+
output_file: 'custom.epub',
|
33
|
+
build_dir: @tmp
|
34
|
+
)
|
35
|
+
assert_equal 'custom.epub', cb.output_file
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_default_build_dir
|
39
|
+
cb = EpubTools::CompileBook.new(
|
40
|
+
title: @title,
|
41
|
+
author: @author,
|
42
|
+
source_dir: @source
|
43
|
+
)
|
44
|
+
assert cb.build_dir.end_with?('.epub_tools_build')
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_initialize_assigns_attributes
|
48
|
+
cb = EpubTools::CompileBook.new(
|
49
|
+
title: @title,
|
50
|
+
author: @author,
|
51
|
+
source_dir: @source,
|
52
|
+
cover_image: 'cover.png',
|
53
|
+
output_file: 'o.epub',
|
54
|
+
build_dir: 'bd',
|
55
|
+
verbose: false
|
56
|
+
)
|
57
|
+
assert_equal @title, cb.title
|
58
|
+
assert_equal @author, cb.author
|
59
|
+
assert_equal @source, cb.source_dir
|
60
|
+
assert_equal 'cover.png', cb.cover_image
|
61
|
+
assert_equal 'o.epub', cb.output_file
|
62
|
+
assert_equal 'bd', cb.build_dir
|
63
|
+
refute cb.verbose
|
64
|
+
end
|
65
|
+
|
66
|
+
def test_clean_build_dir_removes_directory
|
67
|
+
build = File.join(@tmp, 'build')
|
68
|
+
FileUtils.mkdir_p(build)
|
69
|
+
File.write(File.join(build, 'foo'), 'bar')
|
70
|
+
cb = EpubTools::CompileBook.new(
|
71
|
+
title: @title,
|
72
|
+
author: @author,
|
73
|
+
source_dir: @source,
|
74
|
+
build_dir: build
|
75
|
+
)
|
76
|
+
assert Dir.exist?(build)
|
77
|
+
cb.send(:clean_build_dir)
|
78
|
+
refute Dir.exist?(build)
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_prepare_dirs_creates_xhtml_and_chapters_directories
|
82
|
+
build = File.join(@tmp, 'build')
|
83
|
+
cb = EpubTools::CompileBook.new(
|
84
|
+
title: @title,
|
85
|
+
author: @author,
|
86
|
+
source_dir: @source,
|
87
|
+
build_dir: build
|
88
|
+
)
|
89
|
+
cb.send(:prepare_dirs)
|
90
|
+
xhtml_dir = cb.send(:xhtml_dir)
|
91
|
+
chapters_dir = cb.send(:chapters_dir)
|
92
|
+
assert Dir.exist?(xhtml_dir)
|
93
|
+
assert Dir.exist?(chapters_dir)
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_log_outputs_when_verbose
|
97
|
+
cb = EpubTools::CompileBook.new(
|
98
|
+
title: @title,
|
99
|
+
author: @author,
|
100
|
+
source_dir: @source,
|
101
|
+
build_dir: @tmp,
|
102
|
+
verbose: true
|
103
|
+
)
|
104
|
+
assert_output("hello\n") { cb.send(:log, 'hello') }
|
105
|
+
end
|
106
|
+
|
107
|
+
def test_log_silent_when_not_verbose
|
108
|
+
cb = EpubTools::CompileBook.new(
|
109
|
+
title: @title,
|
110
|
+
author: @author,
|
111
|
+
source_dir: @source,
|
112
|
+
build_dir: @tmp,
|
113
|
+
verbose: false
|
114
|
+
)
|
115
|
+
assert_silent { cb.send(:log, 'hello') }
|
116
|
+
end
|
117
|
+
|
118
|
+
def test_validate_sequence_raises_when_no_chapters
|
119
|
+
cb = EpubTools::CompileBook.new(
|
120
|
+
title: @title,
|
121
|
+
author: @author,
|
122
|
+
source_dir: @source,
|
123
|
+
build_dir: @tmp
|
124
|
+
)
|
125
|
+
FileUtils.mkdir_p(cb.send(:chapters_dir))
|
126
|
+
err = assert_raises(RuntimeError) { cb.send(:validate_sequence) }
|
127
|
+
assert_match(/No chapter files found/, err.message)
|
128
|
+
end
|
129
|
+
|
130
|
+
def test_validate_sequence_raises_on_missing_chapters
|
131
|
+
build = File.join(@tmp, 'build')
|
132
|
+
cb = EpubTools::CompileBook.new(
|
133
|
+
title: @title,
|
134
|
+
author: @author,
|
135
|
+
source_dir: @source,
|
136
|
+
build_dir: build
|
137
|
+
)
|
138
|
+
chapters = cb.send(:chapters_dir)
|
139
|
+
FileUtils.mkdir_p(chapters)
|
140
|
+
File.write(File.join(chapters, 'chap_1.xhtml'), '')
|
141
|
+
File.write(File.join(chapters, 'chap_3.xhtml'), '')
|
142
|
+
err = assert_raises(RuntimeError) { cb.send(:validate_sequence) }
|
143
|
+
assert_match(/Missing chapter numbers: 2/, err.message)
|
144
|
+
end
|
145
|
+
|
146
|
+
def test_validate_sequence_succeeds_on_complete_sequence
|
147
|
+
build = File.join(@tmp, 'build')
|
148
|
+
cb = EpubTools::CompileBook.new(
|
149
|
+
title: @title,
|
150
|
+
author: @author,
|
151
|
+
source_dir: @source,
|
152
|
+
build_dir: build
|
153
|
+
)
|
154
|
+
chapters = cb.send(:chapters_dir)
|
155
|
+
FileUtils.mkdir_p(chapters)
|
156
|
+
File.write(File.join(chapters, 'chap_1.xhtml'), '')
|
157
|
+
File.write(File.join(chapters, 'chap_2.xhtml'), '')
|
158
|
+
File.write(File.join(chapters, 'chap_3.xhtml'), '')
|
159
|
+
assert_nil cb.send(:validate_sequence)
|
160
|
+
end
|
161
|
+
|
162
|
+
def test_run_calls_all_steps_in_order
|
163
|
+
cb = EpubTools::CompileBook.new(
|
164
|
+
title: @title,
|
165
|
+
author: @author,
|
166
|
+
source_dir: @source,
|
167
|
+
build_dir: @tmp,
|
168
|
+
output_file: 'o.epub'
|
169
|
+
)
|
170
|
+
seq = []
|
171
|
+
cb.define_singleton_method(:clean_build_dir) { seq << :clean }
|
172
|
+
cb.define_singleton_method(:prepare_dirs) { seq << :prepare }
|
173
|
+
cb.define_singleton_method(:extract_xhtmls) { seq << :extract }
|
174
|
+
cb.define_singleton_method(:split_xhtmls) { seq << :split }
|
175
|
+
cb.define_singleton_method(:validate_sequence) { seq << :validate }
|
176
|
+
cb.define_singleton_method(:initialize_epub) { seq << :init }
|
177
|
+
cb.define_singleton_method(:add_chapters) { seq << :add }
|
178
|
+
cb.define_singleton_method(:pack_epub) { seq << :pack }
|
179
|
+
cb.define_singleton_method(:log) { |msg| seq << [:log, msg] }
|
180
|
+
cb.run
|
181
|
+
expected = [
|
182
|
+
:clean, :prepare, :extract, :split,
|
183
|
+
:validate, :init, :add, :pack,
|
184
|
+
[:log, /Done\. Output EPUB: .*o\.epub/],
|
185
|
+
:clean
|
186
|
+
]
|
187
|
+
assert_equal expected[0..7], seq[0..7]
|
188
|
+
assert seq[8].is_a?(Array)
|
189
|
+
assert_equal :log, seq[8][0]
|
190
|
+
assert_match expected[8][1], seq[8][1]
|
191
|
+
assert_equal expected[9], seq[9]
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
require_relative '../lib/epub_tools/epub_initializer'
|
3
|
+
|
4
|
+
class EpubInitializerTest < Minitest::Test
|
5
|
+
def setup
|
6
|
+
@tmp = Dir.mktmpdir
|
7
|
+
@dest = File.join(@tmp, 'book_out')
|
8
|
+
@title = 'My Title'
|
9
|
+
@author = 'Me'
|
10
|
+
end
|
11
|
+
|
12
|
+
def teardown
|
13
|
+
FileUtils.remove_entry(@tmp)
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_run_creates_basic_structure
|
17
|
+
EpubTools::EpubInitializer.new(@title, @author, @dest).run
|
18
|
+
# Check directories
|
19
|
+
assert Dir.exist?(@dest)
|
20
|
+
assert File.directory?(File.join(@dest, 'META-INF'))
|
21
|
+
assert File.directory?(File.join(@dest, 'OEBPS'))
|
22
|
+
# Check files
|
23
|
+
mimetype = File.join(@dest, 'mimetype')
|
24
|
+
assert File.exist?(mimetype)
|
25
|
+
assert_equal 'application/epub+zip', File.read(mimetype)
|
26
|
+
assert File.exist?(File.join(@dest, 'META-INF', 'container.xml'))
|
27
|
+
assert File.exist?(File.join(@dest, 'OEBPS', 'title.xhtml'))
|
28
|
+
assert File.exist?(File.join(@dest, 'OEBPS', 'nav.xhtml'))
|
29
|
+
assert File.exist?(File.join(@dest, 'OEBPS', 'package.opf'))
|
30
|
+
assert File.exist?(File.join(@dest, 'OEBPS', 'style.css'))
|
31
|
+
# Check content of title.xhtml
|
32
|
+
title_page = File.read(File.join(@dest, 'OEBPS', 'title.xhtml'))
|
33
|
+
assert_includes title_page, "<h1 class=\"title\">#{@title}</h1>"
|
34
|
+
assert_includes title_page, "<p class=\"author\">by #{@author}</p>"
|
35
|
+
# Check package.opf metadata
|
36
|
+
opf = File.read(File.join(@dest, 'OEBPS', 'package.opf'))
|
37
|
+
assert_includes opf, "<dc:title>#{@title}</dc:title>"
|
38
|
+
assert_includes opf, "<dc:creator>#{@author}</dc:creator>"
|
39
|
+
refute_includes opf, 'cover.xhtml'
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_run_with_cover_image
|
43
|
+
# create dummy image
|
44
|
+
cover = File.join(@tmp, 'cover.png')
|
45
|
+
File.write(cover, 'PNGDATA')
|
46
|
+
EpubTools::EpubInitializer.new(@title, @author, @dest, cover).run
|
47
|
+
# Check cover file and page
|
48
|
+
assert File.exist?(File.join(@dest, 'OEBPS', 'cover.png'))
|
49
|
+
assert File.exist?(File.join(@dest, 'OEBPS', 'cover.xhtml'))
|
50
|
+
opf = File.read(File.join(@dest, 'OEBPS', 'package.opf'))
|
51
|
+
assert_includes opf, 'cover-image'
|
52
|
+
assert_includes opf, '<item id="cover-page"'
|
53
|
+
assert_includes opf, '<itemref idref="cover-page"'
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
require_relative '../lib/epub_tools/pack_ebook'
|
3
|
+
require 'zip'
|
4
|
+
|
5
|
+
class PackEbookTest < Minitest::Test
|
6
|
+
def setup
|
7
|
+
@tmp = Dir.mktmpdir
|
8
|
+
@epub_dir = File.join(@tmp, 'my_epub')
|
9
|
+
Dir.mkdir(@epub_dir)
|
10
|
+
end
|
11
|
+
|
12
|
+
def teardown
|
13
|
+
FileUtils.remove_entry(@tmp)
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_run_creates_epub_with_expected_entries
|
17
|
+
# Create minimal EPUB directory
|
18
|
+
File.write(File.join(@epub_dir, 'mimetype'), 'application/epub+zip')
|
19
|
+
FileUtils.mkdir_p(File.join(@epub_dir, 'META-INF'))
|
20
|
+
File.write(File.join(@epub_dir, 'META-INF', 'container.xml'), '<container/>' )
|
21
|
+
FileUtils.mkdir_p(File.join(@epub_dir, 'OEBPS'))
|
22
|
+
File.write(File.join(@epub_dir, 'OEBPS', 'title.xhtml'), '<html/>' )
|
23
|
+
|
24
|
+
output = File.join(@tmp, 'out.epub')
|
25
|
+
EpubTools::PackEbook.new(@epub_dir, output).run
|
26
|
+
|
27
|
+
assert File.exist?(output), 'Expected output EPUB to exist'
|
28
|
+
entries = []
|
29
|
+
Zip::File.open(output) do |zip|
|
30
|
+
zip.each do |entry|
|
31
|
+
entries << { name: entry.name, compression: entry.compression_method }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# First entry should be mimetype, stored without compression
|
36
|
+
assert_equal 'mimetype', entries.first[:name]
|
37
|
+
assert_equal 0, entries.first[:compression]
|
38
|
+
|
39
|
+
# Check presence of other files
|
40
|
+
names = entries.map { |e| e[:name] }
|
41
|
+
assert_includes names, 'META-INF/container.xml'
|
42
|
+
assert_includes names, 'OEBPS/title.xhtml'
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_missing_input_dir_raises_error
|
46
|
+
assert_raises(ArgumentError) do
|
47
|
+
EpubTools::PackEbook.new(File.join(@tmp, 'nonexistent'), 'out.epub').run
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_missing_mimetype_raises_error
|
52
|
+
# Directory exists but no mimetype file
|
53
|
+
dir = File.join(@tmp, 'no_mime')
|
54
|
+
Dir.mkdir(dir)
|
55
|
+
assert_raises(ArgumentError) do
|
56
|
+
EpubTools::PackEbook.new(dir, 'out.epub').run
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_default_output_file_name
|
61
|
+
# Setup minimal structure with mimetype
|
62
|
+
File.write(File.join(@epub_dir, 'mimetype'), 'application/epub+zip')
|
63
|
+
# Run without specifying output; default is "<basename>.epub" in parent
|
64
|
+
EpubTools::PackEbook.new(@epub_dir).run
|
65
|
+
default_path = File.join(@tmp, 'my_epub.epub')
|
66
|
+
assert File.exist?(default_path), "Expected default EPUB at \\#{default_path}"
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
require_relative '../lib/epub_tools/split_chapters'
|
3
|
+
|
4
|
+
class SplitChaptersTest < Minitest::Test
|
5
|
+
def setup
|
6
|
+
@tmp = Dir.mktmpdir
|
7
|
+
@input = File.join(@tmp, 'input.xhtml')
|
8
|
+
@out = File.join(@tmp, 'out')
|
9
|
+
content = <<~HTML
|
10
|
+
<?xml version="1.0"?>
|
11
|
+
<html xmlns="http://www.w3.org/1999/xhtml">
|
12
|
+
<body>
|
13
|
+
<h3>Prologue</h3>
|
14
|
+
<p>Intro text</p>
|
15
|
+
<p>Chapter 1</p>
|
16
|
+
<p>First paragraph</p>
|
17
|
+
<p>Chapter 2</p>
|
18
|
+
<p>Second paragraph</p>
|
19
|
+
</body>
|
20
|
+
</html>
|
21
|
+
HTML
|
22
|
+
File.write(@input, content)
|
23
|
+
end
|
24
|
+
|
25
|
+
def teardown
|
26
|
+
FileUtils.remove_entry(@tmp)
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_run_generates_chapter_files
|
30
|
+
EpubTools::SplitChapters.new(@input, 'BookTitle', @out, 'chap').run
|
31
|
+
files = Dir.children(@out)
|
32
|
+
assert_includes files, 'chap_0.xhtml'
|
33
|
+
assert_includes files, 'chap_1.xhtml'
|
34
|
+
assert_includes files, 'chap_2.xhtml'
|
35
|
+
|
36
|
+
# Prologue
|
37
|
+
prologue = File.read(File.join(@out, 'chap_0.xhtml'))
|
38
|
+
assert_includes prologue, '<h1>Prologue</h1>'
|
39
|
+
assert_includes prologue, 'Intro text'
|
40
|
+
refute_includes prologue, 'Chapter 1'
|
41
|
+
|
42
|
+
# Chapter 1
|
43
|
+
ch1 = File.read(File.join(@out, 'chap_1.xhtml'))
|
44
|
+
assert_includes ch1, '<h1>Chapter 1</h1>'
|
45
|
+
assert_includes ch1, 'First paragraph'
|
46
|
+
refute_includes ch1, 'Chapter 2'
|
47
|
+
|
48
|
+
# Chapter 2
|
49
|
+
ch2 = File.read(File.join(@out, 'chap_2.xhtml'))
|
50
|
+
assert_includes ch2, '<h1>Chapter 2</h1>'
|
51
|
+
assert_includes ch2, 'Second paragraph'
|
52
|
+
end
|
53
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require_relative 'test_helper'
|
3
|
+
require_relative '../lib/epub_tools/text_style_class_finder'
|
4
|
+
|
5
|
+
class TextStyleClassFinderTest < Minitest::Test
|
6
|
+
def setup
|
7
|
+
@tmp = Dir.mktmpdir
|
8
|
+
@xhtml = File.join(@tmp, 'doc.xhtml')
|
9
|
+
content = <<~HTML
|
10
|
+
<html><head><style>
|
11
|
+
.c1 { font-style: italic; }
|
12
|
+
.c2 { font-weight: 700; }
|
13
|
+
.other { color: red; }
|
14
|
+
</style></head><body></body></html>
|
15
|
+
HTML
|
16
|
+
File.write(@xhtml, content)
|
17
|
+
@yaml = File.join(@tmp, 'classes.yaml')
|
18
|
+
end
|
19
|
+
|
20
|
+
def teardown
|
21
|
+
FileUtils.remove_entry(@tmp)
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_finds_italic_and_bold_classes
|
25
|
+
EpubTools::TextStyleClassFinder.new(@xhtml, @yaml).call
|
26
|
+
data = YAML.load_file(@yaml)
|
27
|
+
assert_equal ['c1'], data['italics']
|
28
|
+
assert_equal ['c2'], data['bolds']
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_verbose_mode
|
32
|
+
text = <<~OUTPUT
|
33
|
+
Classes with font-style: italic: c1
|
34
|
+
Classes with font-weight: 700: c2
|
35
|
+
OUTPUT
|
36
|
+
assert_output(text) do
|
37
|
+
EpubTools::TextStyleClassFinder.new(@xhtml, @yaml, verbose: true).call
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
require_relative '../lib/epub_tools/unpack_ebook'
|
3
|
+
require 'zip'
|
4
|
+
|
5
|
+
class UnpackEbookTest < Minitest::Test
|
6
|
+
def setup
|
7
|
+
@tmp = Dir.mktmpdir
|
8
|
+
# Build a minimal EPUB directory for zipping
|
9
|
+
@build_dir = File.join(@tmp, 'build')
|
10
|
+
FileUtils.mkdir_p(File.join(@build_dir, 'META-INF'))
|
11
|
+
FileUtils.mkdir_p(File.join(@build_dir, 'OEBPS'))
|
12
|
+
File.write(File.join(@build_dir, 'mimetype'), 'application/epub+zip')
|
13
|
+
File.write(File.join(@build_dir, 'META-INF', 'container.xml'), '<container/>')
|
14
|
+
File.write(File.join(@build_dir, 'OEBPS', 'title.xhtml'), '<html/>')
|
15
|
+
|
16
|
+
# Create .epub zip file
|
17
|
+
@epub_file = File.join(@tmp, 'test.epub')
|
18
|
+
# Create .epub zip file with absolute src paths to avoid cwd issues
|
19
|
+
Zip::File.open(@epub_file, Zip::File::CREATE) do |zip|
|
20
|
+
# Add mimetype first, uncompressed
|
21
|
+
mime_src = File.join(@build_dir, 'mimetype')
|
22
|
+
zip.add_stored('mimetype', mime_src)
|
23
|
+
# Add directories and files
|
24
|
+
Dir.glob(File.join(@build_dir, '**', '*'), File::FNM_DOTMATCH).sort.each do |src_path|
|
25
|
+
rel_path = src_path.sub(%r{^#{Regexp.escape(@build_dir)}/?}, '')
|
26
|
+
next if rel_path.empty? || rel_path == 'mimetype'
|
27
|
+
if File.directory?(src_path)
|
28
|
+
zip.mkdir(rel_path)
|
29
|
+
else
|
30
|
+
zip.add(rel_path, src_path)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
@dest_dir = File.join(@tmp, 'output')
|
36
|
+
end
|
37
|
+
|
38
|
+
def teardown
|
39
|
+
FileUtils.remove_entry(@tmp)
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_run_extracts_all_entries
|
43
|
+
EpubTools::UnpackEbook.new(@epub_file, @dest_dir).run
|
44
|
+
# Check extracted files
|
45
|
+
assert Dir.exist?(@dest_dir)
|
46
|
+
assert_equal 'application/epub+zip', File.read(File.join(@dest_dir, 'mimetype'))
|
47
|
+
assert File.exist?(File.join(@dest_dir, 'META-INF', 'container.xml'))
|
48
|
+
assert File.exist?(File.join(@dest_dir, 'OEBPS', 'title.xhtml'))
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_missing_epub_raises_error
|
52
|
+
missing = File.join(@tmp, 'nope.epub')
|
53
|
+
error = assert_raises(ArgumentError) do
|
54
|
+
EpubTools::UnpackEbook.new(missing, @dest_dir).run
|
55
|
+
end
|
56
|
+
assert_includes error.message, "does not exist"
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require_relative 'test_helper'
|
3
|
+
require_relative '../lib/epub_tools/xhtml_cleaner'
|
4
|
+
|
5
|
+
class XHTMLCleanerTest < Minitest::Test
|
6
|
+
def setup
|
7
|
+
@tmp = Dir.mktmpdir
|
8
|
+
@config = File.join(@tmp, 'config.yaml')
|
9
|
+
File.write(@config, { 'italics' => ['itclass'], 'bolds' => ['boldclass'] }.to_yaml)
|
10
|
+
@file = File.join(@tmp, 'test.xhtml')
|
11
|
+
content = <<~HTML
|
12
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
13
|
+
<html xmlns="http://www.w3.org/1999/xhtml">
|
14
|
+
<body>
|
15
|
+
<p><span class="itclass">ItalicsOnly</span></p>
|
16
|
+
<p>Keep<span class="plain">This</span></p>
|
17
|
+
<p><span class="boldclass">RemoveMe</span></p>
|
18
|
+
<hr style="page-break-before:always"/>
|
19
|
+
<p class="empty"></p>
|
20
|
+
</body>
|
21
|
+
</html>
|
22
|
+
HTML
|
23
|
+
File.write(@file, content)
|
24
|
+
end
|
25
|
+
|
26
|
+
def teardown
|
27
|
+
FileUtils.remove_entry(@tmp)
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_cleaner_removes_and_transforms_tags
|
31
|
+
EpubTools::XHTMLCleaner.new(@file, @config).call
|
32
|
+
result = File.read(@file)
|
33
|
+
assert_includes result, '<i>ItalicsOnly</i>'
|
34
|
+
assert_includes result, 'KeepThis'
|
35
|
+
refute_includes result, '<span'
|
36
|
+
refute_includes result, '<hr'
|
37
|
+
refute_includes result, 'RemoveMe'
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
require 'zip'
|
3
|
+
require_relative '../lib/epub_tools/xhtml_extractor'
|
4
|
+
|
5
|
+
class XHTMLExtractorTest < Minitest::Test
|
6
|
+
def setup
|
7
|
+
@tmp = Dir.mktmpdir
|
8
|
+
@src = File.join(@tmp, 'src')
|
9
|
+
@tgt = File.join(@tmp, 'tgt')
|
10
|
+
Dir.mkdir(@src)
|
11
|
+
@file = File.join(@src, 'sample.epub')
|
12
|
+
Zip::File.open(@file, Zip::File::CREATE) do |zip|
|
13
|
+
zip.get_output_stream('chapter1.xhtml') { |f| f.write '<html><body><p>One</p></body></html>' }
|
14
|
+
zip.get_output_stream('nav.xhtml') { |f| f.write '<html><body>Nav</body></html>' }
|
15
|
+
zip.get_output_stream('folder/ch2.xhtml') { |f| f.write '<html><body><p>Two</p></body></html>' }
|
16
|
+
end
|
17
|
+
@extractor = EpubTools::XHTMLExtractor.new(source_dir: @src, target_dir: @tgt)
|
18
|
+
end
|
19
|
+
|
20
|
+
def teardown
|
21
|
+
FileUtils.remove_entry(@tmp)
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_extracts_xhtml_excluding_nav
|
25
|
+
@extractor.extract_all
|
26
|
+
files = Dir.children(@tgt)
|
27
|
+
assert_includes files, 'sample_chapter1.xhtml'
|
28
|
+
assert_includes files, 'sample_ch2.xhtml'
|
29
|
+
refute_includes files, 'nav.xhtml'
|
30
|
+
end
|
31
|
+
end
|