epub_tools 0.6.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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +3 -8
  3. data/.gitignore +0 -1
  4. data/.tool-versions +1 -1
  5. data/CLAUDE.md +17 -25
  6. data/Gemfile +5 -8
  7. data/Gemfile.lock +60 -54
  8. data/README.md +31 -4
  9. data/bin/epub-tools +1 -3
  10. data/epub_tools.gemspec +3 -4
  11. data/lib/epub_tools/add_chapters.rb +17 -18
  12. data/lib/epub_tools/append_book.rb +8 -15
  13. data/lib/epub_tools/book_builder.rb +28 -22
  14. data/lib/epub_tools/chapter_order.rb +33 -0
  15. data/lib/epub_tools/chapter_validator.rb +6 -3
  16. data/lib/epub_tools/cli.rb +121 -23
  17. data/lib/epub_tools/compile_book.rb +1 -1
  18. data/lib/epub_tools/epub_initializer.rb +93 -99
  19. data/lib/epub_tools/pack_ebook.rb +2 -14
  20. data/lib/epub_tools/pdf_converter.rb +113 -0
  21. data/lib/epub_tools/pdf_layout.rb +116 -0
  22. data/lib/epub_tools/set_cover.rb +116 -0
  23. data/lib/epub_tools/split_chapters.rb +22 -20
  24. data/lib/epub_tools/style_finder.rb +11 -40
  25. data/lib/epub_tools/unpack_ebook.rb +2 -20
  26. data/lib/epub_tools/version.rb +1 -1
  27. data/lib/epub_tools/xhtml_cleaner.rb +4 -7
  28. data/lib/epub_tools/xhtml_extractor.rb +1 -7
  29. data/lib/epub_tools.rb +2 -1
  30. data/test/add_chapters_test.rb +64 -0
  31. data/test/append_book_test.rb +11 -11
  32. data/test/chapter_validator_test.rb +12 -0
  33. data/test/cli_test.rb +40 -3
  34. data/test/compile_book_test.rb +14 -0
  35. data/test/epub_initializer_test.rb +18 -0
  36. data/test/pdf_converter_test.rb +69 -0
  37. data/test/pdf_layout_test.rb +57 -0
  38. data/test/set_cover_test.rb +68 -0
  39. data/test/split_chapters_test.rb +18 -12
  40. data/test/style_finder_test.rb +3 -9
  41. data/test/test_helper.rb +1 -1
  42. data/test/unpack_ebook_test.rb +1 -1
  43. data/test/xhtml_cleaner_test.rb +1 -4
  44. metadata +14 -35
  45. data/lib/epub_tools/chapter_marker_detector.rb +0 -46
  46. data/lib/epub_tools/cli/command_options_configurator.rb +0 -128
  47. data/lib/epub_tools/cli/command_registry.rb +0 -49
  48. data/lib/epub_tools/cli/option_builder.rb +0 -166
  49. data/lib/epub_tools/cli/runner.rb +0 -114
  50. data/lib/epub_tools/compile_workspace.rb +0 -40
  51. data/lib/epub_tools/epub_configuration.rb +0 -33
  52. data/lib/epub_tools/epub_file_writer.rb +0 -57
  53. data/lib/epub_tools/epub_metadata_builder.rb +0 -92
  54. data/lib/epub_tools/xhtml_generator.rb +0 -71
  55. data/test/cli/command_registry_test.rb +0 -68
  56. data/test/cli/option_builder_test.rb +0 -183
  57. data/test/cli/runner_test.rb +0 -91
  58. data/test/compile_workspace_test.rb +0 -55
@@ -191,3 +191,67 @@ class AddChaptersHalfChapterTest < Minitest::Test
191
191
  XHTML
192
192
  end
193
193
  end
194
+
195
+ class AddChaptersReadingOrderTest < Minitest::Test
196
+ def setup
197
+ @tmp = Dir.mktmpdir
198
+ @chapters_dir = File.join(@tmp, 'chapters')
199
+ @epub_dir = File.join(@tmp, 'OEBPS')
200
+ FileUtils.mkdir_p([@chapters_dir, @epub_dir])
201
+ create_book_with_two_chapters
202
+ write_chapter('chapter_1b.xhtml', '<h1>Chapter 1b: Later</h1>')
203
+ write_chapter('chapter_1a.xhtml', '<h1>Chapter 1a: Side</h1>')
204
+ write_chapter('chapter_3.xhtml', '<p>No heading</p>')
205
+ write_chapter('chapter_epilogue.xhtml', '<h1>Epilogue: After</h1>')
206
+ end
207
+
208
+ def teardown
209
+ FileUtils.remove_entry(@tmp)
210
+ end
211
+
212
+ def test_inserts_chapters_in_reading_order_with_their_headings
213
+ EpubTools::AddChapters.new(chapters_dir: @chapters_dir, oebps_dir: @epub_dir).run
214
+
215
+ opf = Nokogiri::XML(File.read(File.join(@epub_dir, 'package.opf')))
216
+ nav = Nokogiri::XML(File.read(File.join(@epub_dir, 'nav.xhtml'))).remove_namespaces!
217
+ spine = opf.xpath('//xmlns:itemref').map { |i| i['idref'] }
218
+
219
+ assert_equal %w[title chap1 chapter_1a chapter_1b chap2 chap3 chapter_epilogue], spine
220
+ assert_equal ['Title Page', 'Chapter 1', 'Chapter 1a: Side', 'Chapter 1b: Later', 'Chapter 2', 'Chapter 3',
221
+ 'Epilogue: After'],
222
+ nav.xpath('//nav/ol/li/a').map(&:text)
223
+ end
224
+
225
+ private
226
+
227
+ def write_chapter(name, body)
228
+ File.write(File.join(@chapters_dir, name), "<html xmlns=\"http://www.w3.org/1999/xhtml\"><body>#{body}</body></html>")
229
+ end
230
+
231
+ def create_book_with_two_chapters
232
+ File.write(File.join(@epub_dir, 'package.opf'), <<~XML)
233
+ <?xml version="1.0"?>
234
+ <package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="pub-id" xml:lang="en">
235
+ <metadata xmlns:dc="http://purl.org/dc/elements/1.1/"></metadata>
236
+ <manifest>
237
+ <item id="title" href="title.xhtml" media-type="application/xhtml+xml"/>
238
+ <item id="chap1" href="chapter_1.xhtml" media-type="application/xhtml+xml"/>
239
+ <item id="chap2" href="chapter_2.xhtml" media-type="application/xhtml+xml"/>
240
+ </manifest>
241
+ <spine><itemref idref="title"/><itemref idref="chap1"/><itemref idref="chap2"/></spine>
242
+ </package>
243
+ XML
244
+ File.write(File.join(@epub_dir, 'nav.xhtml'), <<~XHTML)
245
+ <?xml version="1.0" encoding="utf-8"?>
246
+ <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" lang="en">
247
+ <body>
248
+ <nav epub:type="toc" id="toc"><ol>
249
+ <li><a href="title.xhtml">Title Page</a></li>
250
+ <li><a href="chapter_1.xhtml">Chapter 1</a></li>
251
+ <li><a href="chapter_2.xhtml">Chapter 2</a></li>
252
+ </ol></nav>
253
+ </body>
254
+ </html>
255
+ XHTML
256
+ end
257
+ end
@@ -45,7 +45,7 @@ class AppendBookTest < Minitest::Test
45
45
 
46
46
  def test_detect_conflicts_raises_on_overlap
47
47
  ab = build_append_book
48
- setup_conflict_dirs(ab, new_chapters: [1, 2], existing_chapters: [1])
48
+ setup_conflict_dirs(new_chapters: [1, 2], existing_chapters: [1])
49
49
 
50
50
  error = assert_raises(ArgumentError) { ab.send(:detect_conflicts) }
51
51
  assert_match(/chapters 1 already exist/, error.message)
@@ -53,11 +53,19 @@ class AppendBookTest < Minitest::Test
53
53
 
54
54
  def test_detect_conflicts_passes_with_no_overlap
55
55
  ab = build_append_book
56
- setup_conflict_dirs(ab, new_chapters: [5], existing_chapters: [1, 2])
56
+ setup_conflict_dirs(new_chapters: [5], existing_chapters: [1, 2])
57
57
 
58
58
  ab.send(:detect_conflicts)
59
59
  end
60
60
 
61
+ def test_detect_conflicts_with_lettered_chapters
62
+ ab = build_append_book
63
+ setup_conflict_dirs(new_chapters: %w[3a 4], existing_chapters: %w[3 3a])
64
+
65
+ error = assert_raises(ArgumentError) { ab.send(:detect_conflicts) }
66
+ assert_match(/chapters 3a already exist/, error.message)
67
+ end
68
+
61
69
  def test_detect_conflicts_with_half_chapters
62
70
  ab = build_append_book
63
71
  chapters_dir = File.join(@tmp, 'chapters')
@@ -67,10 +75,6 @@ class AppendBookTest < Minitest::Test
67
75
  FileUtils.touch(File.join(chapters_dir, 'chapter_3_5.xhtml'))
68
76
  FileUtils.touch(File.join(oebps_dir, 'chapter_3_5.xhtml'))
69
77
 
70
- workspace = ab.instance_variable_get(:@workspace)
71
- workspace.instance_variable_set(:@chapters_dir, chapters_dir)
72
- workspace.instance_variable_set(:@epub_dir, File.join(@tmp, 'epub'))
73
-
74
78
  error = assert_raises(ArgumentError) { ab.send(:detect_conflicts) }
75
79
  assert_match(/3\.5 already exist/, error.message)
76
80
  end
@@ -112,16 +116,12 @@ class AppendBookTest < Minitest::Test
112
116
  )
113
117
  end
114
118
 
115
- def setup_conflict_dirs(append_book, new_chapters:, existing_chapters:)
119
+ def setup_conflict_dirs(new_chapters:, existing_chapters:)
116
120
  chapters_dir = File.join(@tmp, 'chapters')
117
121
  oebps_dir = File.join(@tmp, 'epub', 'OEBPS')
118
122
  FileUtils.mkdir_p([chapters_dir, oebps_dir])
119
123
 
120
124
  new_chapters.each { |n| FileUtils.touch(File.join(chapters_dir, "chapter_#{n}.xhtml")) }
121
125
  existing_chapters.each { |n| FileUtils.touch(File.join(oebps_dir, "chapter_#{n}.xhtml")) }
122
-
123
- workspace = append_book.instance_variable_get(:@workspace)
124
- workspace.instance_variable_set(:@chapters_dir, chapters_dir)
125
- workspace.instance_variable_set(:@epub_dir, File.join(@tmp, 'epub'))
126
126
  end
127
127
  end
@@ -58,6 +58,18 @@ class ChapterValidatorTest < Minitest::Test
58
58
  assert_match(/Missing chapter numbers: 2/, error.message)
59
59
  end
60
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
+
61
73
  private
62
74
 
63
75
  def create_chapter_files(numbers)
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 'cli/command_registry_test'
5
- require_relative 'cli/option_builder_test'
6
- require_relative 'cli/runner_test'
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
@@ -113,4 +113,18 @@ class CompileBookTest < Minitest::Test
113
113
 
114
114
  assert_equal 'test.epub', result
115
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
116
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 &amp; 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 &amp; 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
@@ -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, 'chap_0.xhtml'),
50
- File.join(@out, 'chap_1.xhtml'),
51
- File.join(@out, 'chap_2.xhtml')
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
- ['chap_0.xhtml', 'chap_1.xhtml', 'chap_2.xhtml'].each do |file|
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, 'chap_0.xhtml'))
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, 'chap_1.xhtml'))
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,7 +85,7 @@ class SplitChaptersTest < Minitest::Test
86
85
  end
87
86
 
88
87
  def verify_chapter_two_content
89
- ch2 = File.read(File.join(@out, 'chap_2.xhtml'))
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'
@@ -120,7 +119,7 @@ class SplitChaptersContinuedTest < Minitest::Test
120
119
 
121
120
  def test_splits_continued_chapters
122
121
  result = EpubTools::SplitChapters.new(
123
- input_file: @input, book_title: 'Test', output_dir: @out, output_prefix: 'chapter'
122
+ input_file: @input, book_title: 'Test', output_dir: @out
124
123
  ).run
125
124
 
126
125
  assert_equal 3, result.size
@@ -131,7 +130,7 @@ class SplitChaptersContinuedTest < Minitest::Test
131
130
 
132
131
  def test_continued_chapter_content
133
132
  EpubTools::SplitChapters.new(
134
- input_file: @input, book_title: 'Test', output_dir: @out, output_prefix: 'chapter'
133
+ input_file: @input, book_title: 'Test', output_dir: @out
135
134
  ).run
136
135
 
137
136
  continued = File.read(File.join(@out, 'chapter_1_5.xhtml'))
@@ -140,6 +139,13 @@ class SplitChaptersContinuedTest < Minitest::Test
140
139
  assert_includes continued, 'Continued text'
141
140
  end
142
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
+
143
149
  def test_continued_marker_case_insensitive
144
150
  content = <<~HTML
145
151
  <?xml version="1.0"?>
@@ -155,7 +161,7 @@ class SplitChaptersContinuedTest < Minitest::Test
155
161
  File.write(@input, content)
156
162
 
157
163
  result = EpubTools::SplitChapters.new(
158
- input_file: @input, book_title: 'Test', output_dir: @out, output_prefix: 'chapter'
164
+ input_file: @input, book_title: 'Test', output_dir: @out
159
165
  ).run
160
166
 
161
167
  assert_includes result, File.join(@out, 'chapter_5_5.xhtml')
@@ -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 StyleFinder < Minitest::Test
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, output_path: @yaml).run
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, output_path: @yaml, verbose: true).run
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
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'simplecov'
4
4
  SimpleCov.start do
5
- add_filter '/test/'
5
+ skip '/test/'
6
6
  end
7
7
 
8
8
  require 'minitest/autorun'
@@ -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, '**', '*'), File::FNM_DOTMATCH).sort.each do |src_path|
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
@@ -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, class_config: @config).run
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