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,5 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'command_registry'
2
4
  require_relative 'option_builder'
5
+ require_relative 'command_options_configurator'
3
6
 
4
7
  module EpubTools
5
8
  module CLI
@@ -12,24 +15,15 @@ module EpubTools
12
15
  def initialize(program_name = nil)
13
16
  @registry = CommandRegistry.new
14
17
  @program_name = program_name || File.basename($PROGRAM_NAME)
18
+ @options_configurator = CommandOptionsConfigurator.new
15
19
  end
16
20
 
17
21
  # Run the CLI application
18
22
  # @param args [Array<String>] Command line arguments
19
23
  # @return [Boolean] true if the command was run successfully
20
24
  def run(args = ARGV)
21
- # Handle global version flag
22
- if ['-v', '--version'].include?(args[0])
23
- puts EpubTools::VERSION
24
- exit 0
25
- end
26
-
27
- commands = registry.available_commands
28
-
29
- if args.empty? || !commands.include?(args[0])
30
- print_usage(commands)
31
- exit 1
32
- end
25
+ handle_version_flag(args)
26
+ validate_command_args(args)
33
27
 
34
28
  cmd = args.shift
35
29
  handle_command(cmd, args)
@@ -43,6 +37,36 @@ module EpubTools
43
37
  command_config = registry.get(cmd)
44
38
  return false unless command_config
45
39
 
40
+ builder = build_option_parser(cmd, command_config)
41
+ execute_command(command_config, builder, args)
42
+ end
43
+
44
+ private
45
+
46
+ # Handle version flag and exit if present
47
+ # @param args [Array<String>] Command line arguments
48
+ def handle_version_flag(args)
49
+ return unless ['-v', '--version'].include?(args[0])
50
+
51
+ puts EpubTools::VERSION
52
+ exit 0
53
+ end
54
+
55
+ # Validate command arguments and exit if invalid
56
+ # @param args [Array<String>] Command line arguments
57
+ def validate_command_args(args)
58
+ commands = registry.available_commands
59
+ return unless args.empty? || !commands.include?(args[0])
60
+
61
+ print_usage(commands)
62
+ exit 1
63
+ end
64
+
65
+ # Build option parser for a command
66
+ # @param cmd [String] Command name
67
+ # @param command_config [Hash] Command configuration
68
+ # @return [OptionBuilder] Configured option builder
69
+ def build_option_parser(cmd, command_config)
46
70
  options = command_config[:default_options].dup
47
71
  required_keys = command_config[:required_keys]
48
72
 
@@ -50,114 +74,39 @@ module EpubTools
50
74
  .with_banner("Usage: #{program_name} #{cmd} [options]")
51
75
  .with_help_option
52
76
 
53
- # Configure command-specific options
54
- configure_command_options(cmd, builder)
77
+ @options_configurator.configure(cmd, builder)
78
+ builder
79
+ end
55
80
 
56
- # Parse arguments and run the command
81
+ # Execute a command with parsed options
82
+ # @param command_config [Hash] Command configuration
83
+ # @param builder [OptionBuilder] Option builder instance
84
+ # @param args [Array<String>] Command line arguments
85
+ # @return [Object] Command instance
86
+ def execute_command(command_config, builder, args)
57
87
  options = builder.parse(args)
58
88
  command_class = command_config[:class]
59
- command_class.new(options).run
60
- true
89
+ command_instance = command_class.new(options)
90
+ command_instance.run
91
+ command_instance
61
92
  end
62
93
 
63
- private
64
-
65
94
  # Print usage information
66
95
  # @param commands [Array<String>] Available commands
67
96
  def print_usage(_commands)
68
- puts <<~USAGE
69
- Usage: #{program_name} COMMAND [options]
70
- Commands:
71
- init Initialize a bare-bones EPUB
72
- extract Extract XHTML files from EPUBs
73
- split Split XHTML into separate XHTMLs per chapter
74
- add Add chapter XHTML files into an EPUB
75
- pack Package an EPUB directory into a .epub file
76
- unpack Unpack an EPUB file into a directory
77
- compile Takes EPUBs in a dir and splits, cleans, and compiles into a single EPUB.
78
- USAGE
97
+ puts "Usage: #{program_name} COMMAND [options]"
98
+ puts 'Commands:'
99
+ print_command_list
79
100
  end
80
101
 
81
- # Configure command-specific options
82
- # @param cmd [String] Command name
83
- # @param builder [OptionBuilder] Option builder instance
84
- def configure_command_options(cmd, builder)
85
- case cmd
86
- when 'add'
87
- builder.with_custom_options do |opts, options|
88
- opts.on('-c DIR', '--chapters-dir DIR', 'Chapters directory (required)') { |v| options[:chapters_dir] = v }
89
- opts.on('-e DIR', '--oebps-dir DIR', 'EPUB OEBPS directory (required)') do |v|
90
- options[:oebps_dir] = v
91
- end
92
- end
93
-
94
- when 'extract'
95
- builder.with_custom_options do |opts, options|
96
- opts.on('-s DIR', '--source-dir DIR', 'Directory with EPUBs to extract XHTMLs from (required)') do |v|
97
- options[:source_dir] = v
98
- end
99
- opts.on('-t DIR', '--target-dir DIR',
100
- 'Directory where the XHTML files will be extracted to (required)') do |v|
101
- options[:target_dir] = v
102
- end
103
- end
104
- .with_verbose_option
105
-
106
- when 'split'
107
- builder.with_custom_options do |opts, options|
108
- opts.on('-i FILE', '--input FILE', 'Source XHTML file (required)') { |v| options[:input_file] = v }
109
- opts.on('-t TITLE', '--title TITLE', 'Book title for HTML <title> tags (required)') do |v|
110
- options[:book_title] = v
111
- end
112
- opts.on('-o DIR', '--output-dir DIR',
113
- "Output directory for chapter files (default: #{options[:output_dir]})") do |v|
114
- options[:output_dir] = v
115
- end
116
- opts.on('-p PREFIX', '--prefix PREFIX',
117
- "Filename prefix for chapters (default: #{options[:prefix]})") do |v|
118
- options[:prefix] = v
119
- end
120
- end
121
- .with_verbose_option
122
-
123
- when 'init'
124
- builder.with_title_option
125
- .with_author_option
126
- .with_custom_options do |opts, options|
127
- opts.on('-o DIR', '--output-dir DIR', 'Destination EPUB directory (required)') do |v|
128
- options[:destination] = v
129
- end
130
- end
131
- .with_cover_option
132
-
133
- when 'pack'
134
- builder.with_input_dir('EPUB directory to package')
135
- .with_output_file('Output EPUB file path')
136
- .with_verbose_option
137
-
138
- when 'unpack'
139
- builder.with_custom_options do |opts, options|
140
- opts.on('-i FILE', '--input-file FILE', 'EPUB file to unpack (required)') { |v| options[:epub_file] = v }
141
- opts.on('-o DIR', '--output-dir DIR', 'Output directory to extract into (default: basename of epub)') do |v|
142
- options[:output_dir] = v
143
- end
144
- end
145
- .with_verbose_option
146
-
147
- when 'compile'
148
- builder.with_title_option
149
- .with_author_option
150
- .with_custom_options do |opts, options|
151
- opts.on('-s DIR', '--source-dir DIR', 'Directory with EPUBs to extract XHTMLs from (required)') do |v|
152
- options[:source_dir] = v
153
- end
154
- opts.on('-o FILE', '--output FILE', 'EPUB to create (default: book title in source dir)') do |v|
155
- options[:output_file] = v
156
- end
157
- end
158
- .with_cover_option
159
- .with_verbose_option
160
- end
102
+ def print_command_list
103
+ puts ' init Initialize a bare-bones EPUB'
104
+ puts ' extract Extract XHTML files from EPUBs'
105
+ puts ' split Split XHTML into separate XHTMLs per chapter'
106
+ puts ' add Add chapter XHTML files into an EPUB'
107
+ puts ' pack Package an EPUB directory into a .epub file'
108
+ puts ' unpack Unpack an EPUB file into a directory'
109
+ puts ' compile Takes EPUBs in a dir and splits, cleans, and compiles into a single EPUB.'
161
110
  end
162
111
  end
163
112
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'cli/command_registry'
2
4
  require_relative 'cli/option_builder'
3
5
  require_relative 'cli/runner'
@@ -10,36 +12,21 @@ module EpubTools
10
12
  # @return [CLI::Runner] A configured runner instance
11
13
  def self.create_runner(program_name = nil)
12
14
  runner = Runner.new(program_name)
13
-
14
- # Register all commands
15
- runner.registry.register('add', EpubTools::AddChapters,
16
- %i[chapters_dir oebps_dir])
17
-
18
- runner.registry.register('extract', EpubTools::XHTMLExtractor,
19
- %i[source_dir target_dir],
20
- { verbose: true })
21
-
22
- runner.registry.register('split', EpubTools::SplitChapters,
23
- %i[input_file book_title],
24
- { output_dir: './chapters', prefix: 'chapter', verbose: true })
25
-
26
- runner.registry.register('init', EpubTools::EpubInitializer,
27
- %i[title author destination],
28
- { verbose: true })
29
-
30
- runner.registry.register('pack', EpubTools::PackEbook,
31
- %i[input_dir output_file],
32
- { verbose: true })
33
-
34
- runner.registry.register('unpack', EpubTools::UnpackEbook,
35
- [:epub_file],
36
- { verbose: true })
37
-
38
- runner.registry.register('compile', EpubTools::CompileBook,
39
- %i[title author source_dir],
40
- { verbose: true })
41
-
15
+ register_all_commands(runner.registry)
42
16
  runner
43
17
  end
18
+
19
+ # Register all available commands with their configurations
20
+ # @param registry [CommandRegistry] The command registry to populate
21
+ def self.register_all_commands(registry)
22
+ registry.register('add', EpubTools::AddChapters, %i[chapters_dir oebps_dir])
23
+ registry.register('extract', EpubTools::XHTMLExtractor, %i[source_dir target_dir], { verbose: true })
24
+ registry.register('split', EpubTools::SplitChapters, %i[input_file book_title],
25
+ { output_dir: './chapters', prefix: 'chapter', verbose: true })
26
+ registry.register('init', EpubTools::EpubInitializer, %i[title author destination], { verbose: true })
27
+ registry.register('pack', EpubTools::PackEbook, %i[input_dir output_file], { verbose: true })
28
+ registry.register('unpack', EpubTools::UnpackEbook, [:epub_file], { verbose: true })
29
+ registry.register('compile', EpubTools::CompileBook, %i[title author source_dir], { verbose: true })
30
+ end
44
31
  end
45
32
  end
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
2
4
  require 'fileutils'
3
5
  require_relative 'loggable'
4
6
  require_relative 'xhtml_extractor'
@@ -6,11 +8,14 @@ require_relative 'split_chapters'
6
8
  require_relative 'epub_initializer'
7
9
  require_relative 'add_chapters'
8
10
  require_relative 'pack_ebook'
11
+ require_relative 'compile_workspace'
12
+ require_relative 'chapter_validator'
9
13
 
10
14
  module EpubTools
11
15
  # Orchestrates extraction, splitting, validation, and packaging of book EPUBs
12
16
  class CompileBook
13
17
  include Loggable
18
+
14
19
  # Book title
15
20
  attr_reader :title
16
21
  # Book author
@@ -43,114 +48,92 @@ module EpubTools
43
48
  @output_file = options[:output_file] || default_output_file
44
49
  @build_dir = options[:build_dir] || File.join(Dir.pwd, '.epub_tools_build')
45
50
  @verbose = options[:verbose] || false
51
+ @workspace = CompileWorkspace.new(@build_dir)
46
52
  end
47
53
 
48
54
  # Run the full compile workflow
49
55
  def run
50
- clean_build_dir
51
- prepare_dirs
56
+ setup_workspace
52
57
  extract_xhtmls
53
58
  split_xhtmls
54
- validate_sequence
59
+ validate_chapters
55
60
  initialize_epub
56
61
  add_chapters
57
62
  pack_epub
58
- log "Done. Output EPUB: #{File.expand_path(output_file)}"
59
- clean_build_dir
63
+ finalize_and_cleanup
60
64
  end
61
65
 
62
66
  private
63
67
 
64
- def default_output_file
65
- "#{title.gsub(' ', '_')}.epub"
66
- end
67
-
68
- def clean_build_dir
69
- log "Cleaning build directory #{build_dir}..."
70
- FileUtils.rm_rf(build_dir)
71
- end
72
-
73
- def prepare_dirs
68
+ def setup_workspace
69
+ @workspace.clean
70
+ log "Cleaning build directory #{@build_dir}..."
71
+ @workspace.prepare_directories
74
72
  log 'Preparing build directories...'
75
- FileUtils.mkdir_p(xhtml_dir)
76
- FileUtils.mkdir_p(chapters_dir)
77
73
  end
78
74
 
79
- def xhtml_dir
80
- @xhtml_dir ||= File.join(build_dir, 'xhtml')
81
- end
82
-
83
- def chapters_dir
84
- @chapters_dir ||= File.join(build_dir, 'chapters')
75
+ def finalize_and_cleanup
76
+ log "Done. Output EPUB: #{File.expand_path(output_file)}"
77
+ @workspace.clean
78
+ output_file
85
79
  end
86
80
 
87
- def epub_dir
88
- @epub_dir ||= File.join(build_dir, 'epub')
81
+ def default_output_file
82
+ "#{title.gsub(' ', '_')}.epub"
89
83
  end
90
84
 
91
85
  def extract_xhtmls
92
86
  log "Extracting XHTML files from epubs in '#{source_dir}'..."
93
87
  XHTMLExtractor.new({
94
88
  source_dir: source_dir,
95
- target_dir: xhtml_dir,
89
+ target_dir: @workspace.xhtml_dir,
96
90
  verbose: verbose
97
91
  }).run
98
92
  end
99
93
 
100
94
  def split_xhtmls
101
95
  log 'Splitting XHTML files into chapters...'
102
- Dir.glob(File.join(xhtml_dir, '*.xhtml')).each do |xhtml_file|
103
- base = File.basename(xhtml_file, '.xhtml')
104
- log "Splitting '#{base}'..."
105
- SplitChapters.new({
106
- input_file: xhtml_file,
107
- book_title: title,
108
- output_dir: chapters_dir,
109
- output_prefix: 'chapter',
110
- verbose: verbose
111
- }).run
96
+ Dir.glob(File.join(@workspace.xhtml_dir, '*.xhtml')).each do |xhtml_file|
97
+ split_xhtml_file(xhtml_file)
112
98
  end
113
99
  end
114
100
 
115
- def validate_sequence
116
- log 'Validating chapter sequence...'
117
- nums = Dir.glob(File.join(chapters_dir, '*.xhtml')).map do |file|
118
- if (m = File.basename(file, '.xhtml').match(/_(\d+)\z/))
119
- m[1].to_i
120
- end
121
- end.compact
122
- raise "No chapter files found in #{chapters_dir}" if nums.empty?
101
+ def split_xhtml_file(xhtml_file)
102
+ base = File.basename(xhtml_file, '.xhtml')
103
+ log "Splitting '#{base}'..."
104
+ SplitChapters.new(build_split_options(xhtml_file)).run
105
+ end
123
106
 
124
- sorted = nums.sort.uniq
125
- missing = (sorted.first..sorted.last).to_a - sorted
126
- raise "Missing chapter numbers: #{missing.join(' ')}" if missing.any?
107
+ def build_split_options(xhtml_file)
108
+ {
109
+ input_file: xhtml_file,
110
+ book_title: title,
111
+ output_dir: @workspace.chapters_dir,
112
+ output_prefix: 'chapter',
113
+ verbose: verbose
114
+ }
115
+ end
127
116
 
128
- log "Chapter sequence is complete: #{sorted.first} to #{sorted.last}."
117
+ def validate_chapters
118
+ ChapterValidator.new(chapters_dir: @workspace.chapters_dir, verbose: verbose).validate
129
119
  end
130
120
 
131
121
  def initialize_epub
132
122
  log 'Initializing new EPUB...'
133
- if cover_image
134
- EpubInitializer.new({
135
- title: title,
136
- author: author,
137
- destination: epub_dir,
138
- cover_image: cover_image
139
- }).run
140
- else
141
- EpubInitializer.new({
142
- title: title,
143
- author: author,
144
- destination: epub_dir
145
- }).run
146
- end
123
+ EpubInitializer.new(build_epub_options).run
124
+ end
125
+
126
+ def build_epub_options
127
+ options = { title: title, author: author, destination: @workspace.epub_dir }
128
+ options[:cover_image] = cover_image if cover_image
129
+ options
147
130
  end
148
131
 
149
132
  def add_chapters
150
133
  log 'Adding chapters to EPUB...'
151
134
  AddChapters.new({
152
- chapters_dir: chapters_dir,
153
- epub_dir: File.join(epub_dir, 'OEBPS'),
135
+ chapters_dir: @workspace.chapters_dir,
136
+ epub_dir: File.join(@workspace.epub_dir, 'OEBPS'),
154
137
  verbose: verbose
155
138
  }).run
156
139
  end
@@ -158,7 +141,7 @@ module EpubTools
158
141
  def pack_epub
159
142
  log "Building final EPUB '#{output_file}'..."
160
143
  PackEbook.new({
161
- input_dir: epub_dir,
144
+ input_dir: @workspace.epub_dir,
162
145
  output_file: output_file,
163
146
  verbose: verbose
164
147
  }).run
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module EpubTools
6
+ # Manages the build workspace for book compilation
7
+ class CompileWorkspace
8
+ attr_reader :build_dir
9
+
10
+ def initialize(build_dir)
11
+ @build_dir = build_dir
12
+ end
13
+
14
+ # Cleans the build directory
15
+ def clean
16
+ FileUtils.rm_rf(@build_dir)
17
+ end
18
+
19
+ # Prepares all necessary directories
20
+ def prepare_directories
21
+ FileUtils.mkdir_p(xhtml_dir)
22
+ FileUtils.mkdir_p(chapters_dir)
23
+ end
24
+
25
+ # Gets the XHTML extraction directory
26
+ def xhtml_dir
27
+ @xhtml_dir ||= File.join(@build_dir, 'xhtml')
28
+ end
29
+
30
+ # Gets the chapters directory
31
+ def chapters_dir
32
+ @chapters_dir ||= File.join(@build_dir, 'chapters')
33
+ end
34
+
35
+ # Gets the EPUB build directory
36
+ def epub_dir
37
+ @epub_dir ||= File.join(@build_dir, 'epub')
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'time'
5
+
6
+ module EpubTools
7
+ # Handles configuration parsing and setup for EPUB initialization
8
+ class EpubConfiguration
9
+ attr_reader :title, :author, :destination, :uuid, :modified,
10
+ :cover_image_path, :cover_image_fname, :cover_image_media_type, :verbose
11
+
12
+ def initialize(options = {})
13
+ @title = options.fetch(:title)
14
+ @author = options.fetch(:author)
15
+ @destination = File.expand_path(options.fetch(:destination))
16
+ @uuid = "urn:uuid:#{SecureRandom.uuid}"
17
+ @modified = Time.now.utc.iso8601
18
+ @cover_image_path = options[:cover_image]
19
+ @cover_image_fname = nil
20
+ @cover_image_media_type = nil
21
+ @verbose = options[:verbose] || false
22
+ end
23
+
24
+ def cover_image?
25
+ !@cover_image_path.nil?
26
+ end
27
+
28
+ def update_cover_info(fname, media_type)
29
+ @cover_image_fname = fname
30
+ @cover_image_media_type = media_type
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module EpubTools
6
+ # Handles writing files for EPUB structure
7
+ class EpubFileWriter
8
+ def initialize(destination)
9
+ @destination = destination
10
+ end
11
+
12
+ # Creates the basic EPUB directory structure
13
+ def create_structure
14
+ FileUtils.mkdir_p("#{@destination}/META-INF")
15
+ FileUtils.mkdir_p("#{@destination}/OEBPS")
16
+ end
17
+
18
+ # Writes the mimetype file
19
+ def write_mimetype
20
+ File.write("#{@destination}/mimetype", 'application/epub+zip')
21
+ end
22
+
23
+ # Writes the container.xml file
24
+ def write_container
25
+ content = <<~XML
26
+ <?xml version="1.0" encoding="UTF-8"?>
27
+ <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
28
+ <rootfiles>
29
+ <rootfile full-path="OEBPS/package.opf" media-type="application/oebps-package+xml"/>
30
+ </rootfiles>
31
+ </container>
32
+ XML
33
+ File.write("#{@destination}/META-INF/container.xml", content)
34
+ end
35
+
36
+ # Writes XHTML content to a file
37
+ def write_xhtml(filename, content)
38
+ File.write(File.join(@destination, 'OEBPS', filename), content)
39
+ end
40
+
41
+ # Writes the package.opf file
42
+ def write_package_opf(content)
43
+ File.write(File.join(@destination, 'OEBPS', 'package.opf'), content)
44
+ end
45
+
46
+ # Copies the project style.css to EPUB structure
47
+ def write_style
48
+ src = File.join(Dir.pwd, 'style.css')
49
+ dest = File.join(@destination, 'OEBPS', 'style.css')
50
+ unless File.exist?(src)
51
+ warn "Warning: style.css not found in project root (#{src}), skipping copy."
52
+ return
53
+ end
54
+ FileUtils.cp(src, dest)
55
+ end
56
+ end
57
+ end