epub_tools 0.4.1 → 0.5.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +3 -0
  3. data/.rubocop.yml +10 -17
  4. data/CLAUDE.md +124 -0
  5. data/Gemfile +4 -4
  6. data/Gemfile.lock +39 -34
  7. data/Rakefile +2 -0
  8. data/bin/epub-tools +2 -0
  9. data/epub_tools.gemspec +3 -1
  10. data/lib/epub_tools/add_chapters.rb +47 -29
  11. data/lib/epub_tools/chapter_validator.rb +40 -0
  12. data/lib/epub_tools/cli/command_options_configurator.rb +115 -0
  13. data/lib/epub_tools/cli/command_registry.rb +2 -0
  14. data/lib/epub_tools/cli/option_builder.rb +5 -3
  15. data/lib/epub_tools/cli/runner.rb +59 -110
  16. data/lib/epub_tools/cli.rb +16 -29
  17. data/lib/epub_tools/compile_book.rb +48 -65
  18. data/lib/epub_tools/compile_workspace.rb +40 -0
  19. data/lib/epub_tools/epub_configuration.rb +33 -0
  20. data/lib/epub_tools/epub_file_writer.rb +57 -0
  21. data/lib/epub_tools/epub_initializer.rb +83 -162
  22. data/lib/epub_tools/epub_metadata_builder.rb +92 -0
  23. data/lib/epub_tools/loggable.rb +2 -0
  24. data/lib/epub_tools/pack_ebook.rb +28 -14
  25. data/lib/epub_tools/split_chapters.rb +42 -17
  26. data/lib/epub_tools/style_finder.rb +17 -6
  27. data/lib/epub_tools/unpack_ebook.rb +20 -10
  28. data/lib/epub_tools/version.rb +3 -1
  29. data/lib/epub_tools/xhtml_cleaner.rb +1 -0
  30. data/lib/epub_tools/xhtml_extractor.rb +20 -10
  31. data/lib/epub_tools/xhtml_generator.rb +71 -0
  32. data/lib/epub_tools.rb +2 -0
  33. data/test/add_chapters_test.rb +49 -25
  34. data/test/chapter_validator_test.rb +47 -0
  35. data/test/cli/command_registry_test.rb +2 -0
  36. data/test/cli/option_builder_test.rb +24 -14
  37. data/test/cli/runner_test.rb +15 -15
  38. data/test/cli_commands_test.rb +2 -0
  39. data/test/cli_test.rb +2 -0
  40. data/test/cli_version_test.rb +2 -0
  41. data/test/compile_book_test.rb +17 -102
  42. data/test/compile_workspace_test.rb +55 -0
  43. data/test/epub_initializer_test.rb +55 -27
  44. data/test/pack_ebook_test.rb +33 -9
  45. data/test/split_chapters_test.rb +27 -7
  46. data/test/style_finder_test.rb +2 -0
  47. data/test/test_helper.rb +2 -0
  48. data/test/unpack_ebook_test.rb +45 -20
  49. data/test/xhtml_cleaner_test.rb +2 -0
  50. data/test/xhtml_extractor_test.rb +3 -1
  51. metadata +13 -3
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'test_helper'
2
4
  require_relative '../lib/epub_tools/epub_initializer'
3
5
 
@@ -16,34 +18,10 @@ class EpubInitializerTest < Minitest::Test
16
18
  def test_run_creates_basic_structure
17
19
  result = EpubTools::EpubInitializer.new(title: @title, author: @author, destination: @dest).run
18
20
 
19
- # Check return value is the destination directory
20
21
  assert_equal @dest, result
21
-
22
- # Check directories
23
- assert Dir.exist?(@dest)
24
- assert File.directory?(File.join(@dest, 'META-INF'))
25
- assert File.directory?(File.join(@dest, 'OEBPS'))
26
- # Check files
27
- mimetype = File.join(@dest, 'mimetype')
28
-
29
- assert_path_exists mimetype
30
- assert_equal 'application/epub+zip', File.read(mimetype)
31
- assert_path_exists File.join(@dest, 'META-INF', 'container.xml')
32
- assert_path_exists File.join(@dest, 'OEBPS', 'title.xhtml')
33
- assert_path_exists File.join(@dest, 'OEBPS', 'nav.xhtml')
34
- assert_path_exists File.join(@dest, 'OEBPS', 'package.opf')
35
- assert_path_exists File.join(@dest, 'OEBPS', 'style.css')
36
- # Check content of title.xhtml
37
- title_page = File.read(File.join(@dest, 'OEBPS', 'title.xhtml'))
38
-
39
- assert_includes title_page, "<h1 class=\"title\">#{@title}</h1>"
40
- assert_includes title_page, "<p class=\"author\">by #{@author}</p>"
41
- # Check package.opf metadata
42
- opf = File.read(File.join(@dest, 'OEBPS', 'package.opf'))
43
-
44
- assert_includes opf, "<dc:title>#{@title}</dc:title>"
45
- assert_includes opf, "<dc:creator>#{@author}</dc:creator>"
46
- refute_includes opf, 'cover.xhtml'
22
+ verify_directory_structure
23
+ verify_required_files
24
+ verify_file_contents
47
25
  end
48
26
 
49
27
  def test_run_with_cover_image
@@ -136,4 +114,54 @@ class EpubInitializerTest < Minitest::Test
136
114
 
137
115
  refute_includes opf, 'cover-image'
138
116
  end
117
+
118
+ private
119
+
120
+ def verify_directory_structure
121
+ assert Dir.exist?(@dest)
122
+ assert File.directory?(File.join(@dest, 'META-INF'))
123
+ assert File.directory?(File.join(@dest, 'OEBPS'))
124
+ end
125
+
126
+ def verify_required_files
127
+ required_files = [
128
+ 'mimetype',
129
+ File.join('META-INF', 'container.xml'),
130
+ File.join('OEBPS', 'title.xhtml'),
131
+ File.join('OEBPS', 'nav.xhtml'),
132
+ File.join('OEBPS', 'package.opf'),
133
+ File.join('OEBPS', 'style.css')
134
+ ]
135
+
136
+ required_files.each do |file|
137
+ assert_path_exists File.join(@dest, file)
138
+ end
139
+ end
140
+
141
+ def verify_file_contents
142
+ verify_mimetype_content
143
+ verify_title_page_content
144
+ verify_opf_metadata
145
+ end
146
+
147
+ def verify_mimetype_content
148
+ mimetype = File.read(File.join(@dest, 'mimetype'))
149
+
150
+ assert_equal 'application/epub+zip', mimetype
151
+ end
152
+
153
+ def verify_title_page_content
154
+ title_page = File.read(File.join(@dest, 'OEBPS', 'title.xhtml'))
155
+
156
+ assert_includes title_page, "<h1 class=\"title\">#{@title}</h1>"
157
+ assert_includes title_page, "<p class=\"author\">by #{@author}</p>"
158
+ end
159
+
160
+ def verify_opf_metadata
161
+ opf = File.read(File.join(@dest, 'OEBPS', 'package.opf'))
162
+
163
+ assert_includes opf, "<dc:title>#{@title}</dc:title>"
164
+ assert_includes opf, "<dc:creator>#{@author}</dc:creator>"
165
+ refute_includes opf, 'cover.xhtml'
166
+ end
139
167
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'test_helper'
2
4
  require_relative '../lib/epub_tools/pack_ebook'
3
5
  require 'zip'
@@ -14,37 +16,59 @@ class PackEbookTest < Minitest::Test
14
16
  end
15
17
 
16
18
  def test_run_creates_epub_with_expected_entries
17
- # Create minimal EPUB directory
19
+ create_minimal_epub_structure
20
+ output = File.join(@tmp, 'out.epub')
21
+ result = EpubTools::PackEbook.new(input_dir: @epub_dir, output_file: output).run
22
+
23
+ verify_return_value(output, result)
24
+ verify_epub_entries(output)
25
+ end
26
+
27
+ private
28
+
29
+ def create_minimal_epub_structure
18
30
  File.write(File.join(@epub_dir, 'mimetype'), 'application/epub+zip')
19
31
  FileUtils.mkdir_p(File.join(@epub_dir, 'META-INF'))
20
32
  File.write(File.join(@epub_dir, 'META-INF', 'container.xml'), '<container/>')
21
33
  FileUtils.mkdir_p(File.join(@epub_dir, 'OEBPS'))
22
34
  File.write(File.join(@epub_dir, 'OEBPS', 'title.xhtml'), '<html/>')
35
+ end
23
36
 
24
- output = File.join(@tmp, 'out.epub')
25
- result = EpubTools::PackEbook.new(input_dir: @epub_dir, output_file: output).run
37
+ def verify_return_value(expected_output, result)
38
+ assert_equal expected_output, result
39
+ assert_path_exists expected_output, 'Expected output EPUB to exist'
40
+ end
41
+
42
+ def verify_epub_entries(output_file)
43
+ entries = extract_zip_entries(output_file)
44
+ verify_mimetype_entry(entries)
45
+ verify_expected_files(entries)
46
+ end
26
47
 
27
- # Check return value is the output file path
28
- assert_equal output, result
29
- assert_path_exists output, 'Expected output EPUB to exist'
48
+ def extract_zip_entries(output_file)
30
49
  entries = []
31
- Zip::File.open(output) do |zip|
50
+ Zip::File.open(output_file) do |zip|
32
51
  zip.each do |entry|
33
52
  entries << { name: entry.name, compression: entry.compression_method }
34
53
  end
35
54
  end
55
+ entries
56
+ end
36
57
 
37
- # First entry should be mimetype, stored without compression
58
+ def verify_mimetype_entry(entries)
38
59
  assert_equal 'mimetype', entries.first[:name]
39
60
  assert_equal 0, entries.first[:compression]
61
+ end
40
62
 
41
- # Check presence of other files
63
+ def verify_expected_files(entries)
42
64
  names = entries.map { |e| e[:name] }
43
65
 
44
66
  assert_includes names, 'META-INF/container.xml'
45
67
  assert_includes names, 'OEBPS/title.xhtml'
46
68
  end
47
69
 
70
+ public
71
+
48
72
  def test_missing_input_dir_raises_error
49
73
  assert_raises(ArgumentError) do
50
74
  EpubTools::PackEbook.new(input_dir: File.join(@tmp, 'nonexistent'), output_file: 'out.epub').run
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'test_helper'
2
4
  require_relative '../lib/epub_tools/split_chapters'
3
5
 
@@ -30,10 +32,19 @@ class SplitChaptersTest < Minitest::Test
30
32
  result = EpubTools::SplitChapters.new(input_file: @input, book_title: 'BookTitle', output_dir: @out,
31
33
  output_prefix: 'chap').run
32
34
 
33
- # Check return value is an array of chapter file paths
35
+ verify_return_value(result)
36
+ verify_generated_files_exist(result)
37
+ verify_chapter_contents
38
+ end
39
+
40
+ private
41
+
42
+ def verify_return_value(result)
34
43
  assert_instance_of Array, result
35
44
  assert_equal 3, result.size
45
+ end
36
46
 
47
+ def verify_generated_files_exist(result)
37
48
  expected_paths = [
38
49
  File.join(@out, 'chap_0.xhtml'),
39
50
  File.join(@out, 'chap_1.xhtml'),
@@ -47,25 +58,34 @@ class SplitChaptersTest < Minitest::Test
47
58
 
48
59
  files = Dir.children(@out)
49
60
 
50
- assert_includes files, 'chap_0.xhtml'
51
- assert_includes files, 'chap_1.xhtml'
52
- assert_includes files, 'chap_2.xhtml'
61
+ ['chap_0.xhtml', 'chap_1.xhtml', 'chap_2.xhtml'].each do |file|
62
+ assert_includes files, file
63
+ end
64
+ end
65
+
66
+ def verify_chapter_contents
67
+ verify_prologue_content
68
+ verify_chapter_one_content
69
+ verify_chapter_two_content
70
+ end
53
71
 
54
- # Prologue
72
+ def verify_prologue_content
55
73
  prologue = File.read(File.join(@out, 'chap_0.xhtml'))
56
74
 
57
75
  assert_includes prologue, '<h1>Prologue</h1>'
58
76
  assert_includes prologue, 'Intro text'
59
77
  refute_includes prologue, 'Chapter 1'
78
+ end
60
79
 
61
- # Chapter 1
80
+ def verify_chapter_one_content
62
81
  ch1 = File.read(File.join(@out, 'chap_1.xhtml'))
63
82
 
64
83
  assert_includes ch1, '<h1>Chapter 1</h1>'
65
84
  assert_includes ch1, 'First paragraph'
66
85
  refute_includes ch1, 'Chapter 2'
86
+ end
67
87
 
68
- # Chapter 2
88
+ def verify_chapter_two_content
69
89
  ch2 = File.read(File.join(@out, 'chap_2.xhtml'))
70
90
 
71
91
  assert_includes ch2, '<h1>Chapter 2</h1>'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'yaml'
2
4
  require_relative 'test_helper'
3
5
  require_relative '../lib/epub_tools/style_finder'
data/test/test_helper.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'simplecov'
2
4
  SimpleCov.start do
3
5
  add_filter '/test/'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'test_helper'
2
4
  require_relative '../lib/epub_tools/unpack_ebook'
3
5
  require 'zip'
@@ -5,37 +7,60 @@ require 'zip'
5
7
  class UnpackEbookTest < Minitest::Test
6
8
  def setup
7
9
  @tmp = Dir.mktmpdir
8
- # Build a minimal EPUB directory for zipping
9
10
  @build_dir = File.join(@tmp, 'build')
11
+ @epub_file = File.join(@tmp, 'test.epub')
12
+ @dest_dir = File.join(@tmp, 'output')
13
+
14
+ create_epub_build_directory
15
+ create_zip_file_from_build_dir
16
+ end
17
+
18
+ private
19
+
20
+ def create_epub_build_directory
10
21
  FileUtils.mkdir_p(File.join(@build_dir, 'META-INF'))
11
22
  FileUtils.mkdir_p(File.join(@build_dir, 'OEBPS'))
23
+
24
+ write_epub_files
25
+ end
26
+
27
+ def write_epub_files
12
28
  File.write(File.join(@build_dir, 'mimetype'), 'application/epub+zip')
13
29
  File.write(File.join(@build_dir, 'META-INF', 'container.xml'), '<container/>')
14
30
  File.write(File.join(@build_dir, 'OEBPS', 'title.xhtml'), '<html/>')
31
+ end
15
32
 
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
-
28
- if File.directory?(src_path)
29
- zip.mkdir(rel_path)
30
- else
31
- zip.add(rel_path, src_path)
32
- end
33
- end
33
+ def create_zip_file_from_build_dir
34
+ Zip::File.open(@epub_file, create: true) do |zip|
35
+ add_mimetype_to_zip(zip)
36
+ add_remaining_files_to_zip(zip)
34
37
  end
38
+ end
35
39
 
36
- @dest_dir = File.join(@tmp, 'output')
40
+ def add_mimetype_to_zip(zip)
41
+ mime_src = File.join(@build_dir, 'mimetype')
42
+ zip.add_stored('mimetype', mime_src)
43
+ end
44
+
45
+ def add_remaining_files_to_zip(zip)
46
+ Dir.glob(File.join(@build_dir, '**', '*'), File::FNM_DOTMATCH).sort.each do |src_path|
47
+ add_file_or_directory_to_zip(zip, src_path)
48
+ end
37
49
  end
38
50
 
51
+ def add_file_or_directory_to_zip(zip, src_path)
52
+ rel_path = src_path.sub(%r{^#{Regexp.escape(@build_dir)}/?}, '')
53
+ return if rel_path.empty? || rel_path == 'mimetype'
54
+
55
+ if File.directory?(src_path)
56
+ zip.mkdir(rel_path)
57
+ else
58
+ zip.add(rel_path, src_path)
59
+ end
60
+ end
61
+
62
+ public
63
+
39
64
  def teardown
40
65
  FileUtils.remove_entry(@tmp)
41
66
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'yaml'
2
4
  require_relative 'test_helper'
3
5
  require_relative '../lib/epub_tools/xhtml_cleaner'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'test_helper'
2
4
  require 'zip'
3
5
  require_relative '../lib/epub_tools/xhtml_extractor'
@@ -9,7 +11,7 @@ class XHTMLExtractorTest < Minitest::Test
9
11
  @tgt = File.join(@tmp, 'tgt')
10
12
  Dir.mkdir(@src)
11
13
  @file = File.join(@src, 'sample.epub')
12
- Zip::File.open(@file, Zip::File::CREATE) do |zip|
14
+ Zip::File.open(@file, create: true) do |zip|
13
15
  zip.get_output_stream('chapter1.xhtml') { |f| f.write '<html><body><p>One</p></body></html>' }
14
16
  zip.get_output_stream('nav.xhtml') { |f| f.write '<html><body>Nav</body></html>' }
15
17
  zip.get_output_stream('folder/ch2.xhtml') { |f| f.write '<html><body><p>Two</p></body></html>' }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: epub_tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaime Rodas
@@ -43,14 +43,14 @@ dependencies:
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '2.4'
46
+ version: '3.2'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '2.4'
53
+ version: '3.2'
54
54
  email:
55
55
  - rodas@hey.com
56
56
  executables:
@@ -64,6 +64,7 @@ files:
64
64
  - ".nova/Configuration.json"
65
65
  - ".rubocop.yml"
66
66
  - ".tool-versions"
67
+ - CLAUDE.md
67
68
  - Gemfile
68
69
  - Gemfile.lock
69
70
  - LICENSE
@@ -73,12 +74,18 @@ files:
73
74
  - epub_tools.gemspec
74
75
  - lib/epub_tools.rb
75
76
  - lib/epub_tools/add_chapters.rb
77
+ - lib/epub_tools/chapter_validator.rb
76
78
  - lib/epub_tools/cli.rb
79
+ - lib/epub_tools/cli/command_options_configurator.rb
77
80
  - lib/epub_tools/cli/command_registry.rb
78
81
  - lib/epub_tools/cli/option_builder.rb
79
82
  - lib/epub_tools/cli/runner.rb
80
83
  - lib/epub_tools/compile_book.rb
84
+ - lib/epub_tools/compile_workspace.rb
85
+ - lib/epub_tools/epub_configuration.rb
86
+ - lib/epub_tools/epub_file_writer.rb
81
87
  - lib/epub_tools/epub_initializer.rb
88
+ - lib/epub_tools/epub_metadata_builder.rb
82
89
  - lib/epub_tools/loggable.rb
83
90
  - lib/epub_tools/pack_ebook.rb
84
91
  - lib/epub_tools/split_chapters.rb
@@ -87,8 +94,10 @@ files:
87
94
  - lib/epub_tools/version.rb
88
95
  - lib/epub_tools/xhtml_cleaner.rb
89
96
  - lib/epub_tools/xhtml_extractor.rb
97
+ - lib/epub_tools/xhtml_generator.rb
90
98
  - style.css
91
99
  - test/add_chapters_test.rb
100
+ - test/chapter_validator_test.rb
92
101
  - test/cli/command_registry_test.rb
93
102
  - test/cli/option_builder_test.rb
94
103
  - test/cli/runner_test.rb
@@ -96,6 +105,7 @@ files:
96
105
  - test/cli_test.rb
97
106
  - test/cli_version_test.rb
98
107
  - test/compile_book_test.rb
108
+ - test/compile_workspace_test.rb
99
109
  - test/epub_initializer_test.rb
100
110
  - test/pack_ebook_test.rb
101
111
  - test/split_chapters_test.rb