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.
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 +20 -24
  6. data/Gemfile +5 -8
  7. data/Gemfile.lock +60 -54
  8. data/README.md +65 -25
  9. data/bin/epub-tools +1 -3
  10. data/epub_tools.gemspec +3 -4
  11. data/lib/epub_tools/add_chapters.rb +26 -14
  12. data/lib/epub_tools/append_book.rb +74 -0
  13. data/lib/epub_tools/book_builder.rb +114 -0
  14. data/lib/epub_tools/chapter_order.rb +33 -0
  15. data/lib/epub_tools/chapter_validator.rb +20 -7
  16. data/lib/epub_tools/cli.rb +121 -22
  17. data/lib/epub_tools/compile_book.rb +12 -126
  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 +35 -70
  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 +4 -0
  30. data/test/add_chapters_test.rb +134 -0
  31. data/test/append_book_test.rb +127 -0
  32. data/test/chapter_validator_test.rb +39 -0
  33. data/test/cli_commands_test.rb +9 -0
  34. data/test/cli_test.rb +40 -3
  35. data/test/compile_book_test.rb +15 -2
  36. data/test/epub_initializer_test.rb +18 -0
  37. data/test/pdf_converter_test.rb +69 -0
  38. data/test/pdf_layout_test.rb +57 -0
  39. data/test/set_cover_test.rb +68 -0
  40. data/test/split_chapters_test.rb +84 -9
  41. data/test/style_finder_test.rb +3 -9
  42. data/test/test_helper.rb +1 -1
  43. data/test/unpack_ebook_test.rb +1 -1
  44. data/test/xhtml_cleaner_test.rb +1 -4
  45. metadata +17 -34
  46. data/lib/epub_tools/cli/command_options_configurator.rb +0 -115
  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 -113
  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
@@ -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
@@ -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 '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
@@ -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.initialize_epub; end
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 &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,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, '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'
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
@@ -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