epuber 0.8.0 → 0.9.1

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +9 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +2 -2
  5. data/epuber.gemspec +3 -6
  6. data/lib/epuber/book/contributor.rb +10 -6
  7. data/lib/epuber/book/file_request.rb +2 -2
  8. data/lib/epuber/book/target.rb +10 -10
  9. data/lib/epuber/book.rb +2 -2
  10. data/lib/epuber/checker/text_checker.rb +14 -6
  11. data/lib/epuber/checker_transformer_base.rb +1 -1
  12. data/lib/epuber/command/build.rb +6 -1
  13. data/lib/epuber/command/from_file.rb +39 -0
  14. data/lib/epuber/command/init.rb +11 -9
  15. data/lib/epuber/command/server.rb +1 -1
  16. data/lib/epuber/command.rb +1 -0
  17. data/lib/epuber/compiler/file_database.rb +2 -2
  18. data/lib/epuber/compiler/file_finders/abstract.rb +3 -3
  19. data/lib/epuber/compiler/file_resolver.rb +3 -2
  20. data/lib/epuber/compiler/file_types/abstract_file.rb +1 -3
  21. data/lib/epuber/compiler/file_types/bade_file.rb +9 -9
  22. data/lib/epuber/compiler/file_types/css_file.rb +84 -0
  23. data/lib/epuber/compiler/file_types/source_file.rb +31 -0
  24. data/lib/epuber/compiler/file_types/stylus_file.rb +4 -3
  25. data/lib/epuber/compiler/nav_generator.rb +5 -5
  26. data/lib/epuber/compiler/opf_generator.rb +4 -4
  27. data/lib/epuber/compiler/xhtml_processor.rb +7 -25
  28. data/lib/epuber/compiler.rb +12 -7
  29. data/lib/epuber/config.rb +3 -3
  30. data/lib/epuber/dsl/attribute.rb +1 -1
  31. data/lib/epuber/dsl/attribute_support.rb +4 -4
  32. data/lib/epuber/dsl/object.rb +2 -2
  33. data/lib/epuber/from_file/bookspec_generator.rb +371 -0
  34. data/lib/epuber/from_file/encryption_handler.rb +146 -0
  35. data/lib/epuber/from_file/from_file_executor.rb +140 -0
  36. data/lib/epuber/from_file/nav_file.rb +163 -0
  37. data/lib/epuber/from_file/opf_file.rb +219 -0
  38. data/lib/epuber/plugin.rb +1 -1
  39. data/lib/epuber/server.rb +17 -17
  40. data/lib/epuber/transformer/book_transformer.rb +108 -0
  41. data/lib/epuber/transformer.rb +2 -0
  42. data/lib/epuber/user_interface.rb +2 -2
  43. data/lib/epuber/vendor/ruby_templater.rb +3 -3
  44. data/lib/epuber/vendor/version.rb +3 -3
  45. data/lib/epuber/version.rb +1 -1
  46. metadata +40 -59
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+ require 'uuidtools'
5
+ require 'digest'
6
+
7
+ module Epuber
8
+ class EncryptionHandler
9
+ class EncryptionItem
10
+ # Encryption algorithm (probably EncryptionHandler::ADOBE_OBFUSCATION or EncryptionHandler::IDPF_OBFUSCATION)
11
+ #
12
+ # @return [String]
13
+ #
14
+ attr_accessor :algorithm
15
+
16
+ # Absolute path to file (from root of EPUB)
17
+ #
18
+ # @return [String]
19
+ #
20
+ attr_accessor :file_path
21
+
22
+ # Encryption key for this file
23
+ #
24
+ # @return [String, nil]
25
+ #
26
+ attr_accessor :key
27
+
28
+ # @param [String] algorithm
29
+ # @param [String] file_path
30
+ #
31
+ def initialize(algorithm, file_path)
32
+ @algorithm = algorithm
33
+ @file_path = file_path
34
+ end
35
+ end
36
+
37
+ ADOBE_OBFUSCATION = 'http://ns.adobe.com/pdf/enc#RC'
38
+ IDPF_OBFUSCATION = 'http://www.idpf.org/2008/embedding'
39
+
40
+ # @return [Hash<String, EncryptionItem>] key is abs file path (from root of EPUB), value is EncryptionItem
41
+ #
42
+ attr_reader :encryption_items
43
+
44
+ # @param [String] encryption_file contents of META-INF/encryption.xml file
45
+ # @param [Epuber::OpfFile] opf
46
+ #
47
+ def initialize(encryption_file, opf)
48
+ @opf = opf
49
+ @encryption_items = _prepare_items(encryption_file)
50
+ end
51
+
52
+ # @param [String] path
53
+ # @param [String] data
54
+ def process_file(path, data)
55
+ enc_item = @encryption_items[path]
56
+ data = EncryptionHandler.decrypt_data(enc_item.key, data, enc_item.algorithm) if enc_item
57
+
58
+ data
59
+ end
60
+
61
+ # Decrypt data with given key and algorithm
62
+ #
63
+ # @param [String] key
64
+ # @param [String] data
65
+ # @param [String] algorithm
66
+ #
67
+ def self.decrypt_data(key, data, algorithm)
68
+ is_adobe = algorithm == ADOBE_OBFUSCATION
69
+ crypt_len = is_adobe ? 1024 : 1040
70
+ crypt = data.byteslice(0, crypt_len)
71
+ .bytes
72
+ key_cycle = key.bytes
73
+ .cycle
74
+ decrypt = crypt.each_with_object([]) { |x, acc| acc << (x ^ key_cycle.next) }
75
+ .pack('C*')
76
+ decrypt + data.byteslice(crypt_len..-1)
77
+ end
78
+
79
+ # Parse IDPF key from unique identifier (main identifier from OPF file)
80
+ #
81
+ # @param [String] raw_unique_identifier
82
+ #
83
+ # @return [String, nil]
84
+ #
85
+ def self.parse_idpf_key(raw_unique_identifier)
86
+ key = raw_unique_identifier.strip.gsub(/[\u0020\u0009\u000d\u000a]/, '')
87
+ Digest::SHA1.digest(key)
88
+ end
89
+
90
+ # @param [String] raw_unique_identifier
91
+ # @param [Array<Nokogiri::XML::Node>] identifiers
92
+ #
93
+ # @return [String, nil]
94
+ #
95
+ def self.find_and_parse_encryption_key(identifiers)
96
+ raw_identifier = identifiers.find do |i|
97
+ i['scheme']&.downcase == 'uuid' || i.text.strip.start_with?('urn:uuid:')
98
+ end&.text&.strip
99
+ return nil unless raw_identifier
100
+
101
+ uuid_str = raw_identifier.sub(/^urn:uuid:/, '')
102
+ UUIDTools::UUID.parse(uuid_str).raw
103
+ end
104
+
105
+ # Parse META-INF/encryption.xml file
106
+ #
107
+ # @return [Array<EncryptionItem>, nil]
108
+ #
109
+ def self.parse_encryption_file(string)
110
+ doc = Nokogiri::XML(string)
111
+ doc.remove_namespaces!
112
+
113
+ encryption_node = doc.at_css('encryption')
114
+ return nil unless encryption_node
115
+
116
+ encryption_node.css('EncryptedData')
117
+ .map do |encrypted_data_node|
118
+ algorithm = encrypted_data_node.at_css('EncryptionMethod')['Algorithm']
119
+ file_path = encrypted_data_node.at_css('CipherData CipherReference')['URI']
120
+
121
+ EncryptionItem.new(algorithm, file_path)
122
+ end
123
+ end
124
+
125
+ # Prepare encryption items with correct keys
126
+ #
127
+ # @param [String] encryption_file
128
+ #
129
+ # @return [Hash<String, EncryptionItem>]
130
+ #
131
+ def _prepare_items(encryption_file)
132
+ idpf_key = EncryptionHandler.parse_idpf_key(@opf.raw_unique_identifier)
133
+ adobe_key = EncryptionHandler.find_and_parse_encryption_key(@opf.identifiers)
134
+
135
+ items = EncryptionHandler.parse_encryption_file(encryption_file)
136
+ items.each do |i|
137
+ if i.algorithm == EncryptionHandler::IDPF_OBFUSCATION
138
+ i.key = idpf_key
139
+ elsif i.algorithm == EncryptionHandler::ADOBE_OBFUSCATION
140
+ i.key = adobe_key
141
+ end
142
+ end
143
+ items.map { |i| [i.file_path, i] }.to_h
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zip'
4
+
5
+ require_relative 'bookspec_generator'
6
+ require_relative 'opf_file'
7
+ require_relative 'nav_file'
8
+ require_relative 'encryption_handler'
9
+
10
+ module Epuber
11
+ class FromFileExecutor
12
+ MIMETYPE_PATH = 'mimetype'
13
+
14
+ ENCRYPTION_PATH = 'META-INF/encryption.xml'
15
+ CONTAINER_PATH = 'META-INF/container.xml'
16
+
17
+ # @param [String] filepath path to EPUB file
18
+ #
19
+ def initialize(filepath)
20
+ @filepath = filepath
21
+ end
22
+
23
+ def run
24
+ UI.puts "📖 Loading EPUB file #{@filepath}"
25
+
26
+ Zip::File.open(@filepath) do |zip_file|
27
+ @zip_file = zip_file
28
+
29
+ validate_mimetype
30
+
31
+ @opf_path = content_opf_path
32
+ UI.puts " Parsing OPF file at #{@opf_path}"
33
+ @opf = OpfFile.new(zip_file.read(@opf_path))
34
+
35
+ if zip_file.find_entry(ENCRYPTION_PATH)
36
+ UI.puts ' Parsing encryption.xml file'
37
+ @encryption_handler = EncryptionHandler.new(zip_file.read(ENCRYPTION_PATH), @opf)
38
+ end
39
+
40
+ UI.puts ' Generating bookspec file'
41
+ basename = File.basename(@filepath, File.extname(@filepath))
42
+ File.write("#{basename}.bookspec", generate_bookspec)
43
+
44
+ export_files
45
+
46
+ UI.puts '' # empty line
47
+ UI.puts <<~TEXT.rstrip.ansi.green
48
+ 🎉 Project initialized.
49
+ Please review generated #{basename}.bookspec file and start using Epuber.
50
+
51
+ For more information about Epuber, please visit https://github.com/epuber-io/epuber/tree/master/docs.
52
+ TEXT
53
+ end
54
+ end
55
+
56
+ # @param [Zip::File] zip_file
57
+ #
58
+ # @return [void]
59
+ #
60
+ def validate_mimetype
61
+ entry = @zip_file.find_entry(MIMETYPE_PATH)
62
+ UI.error! "This is not valid EPUB file (#{MIMETYPE_PATH} file is missing)" if entry.nil?
63
+
64
+ mimetype = @zip_file.read(entry)
65
+
66
+ return if mimetype == 'application/epub+zip'
67
+
68
+ UI.error! <<~MSG
69
+ This is not valid EPUB file (#{MIMETYPE_PATH} file does not contain required application/epub+zip, it is #{mimetype} instead)
70
+ MSG
71
+ end
72
+
73
+ # @param [Zip::File] zip_file
74
+ #
75
+ # @return [String]
76
+ def content_opf_path
77
+ entry = @zip_file.find_entry(CONTAINER_PATH)
78
+ UI.error! "This is not valid EPUB file (#{CONTAINER_PATH} file is missing)" if entry.nil?
79
+
80
+ doc = Nokogiri::XML(@zip_file.read(entry))
81
+ doc.remove_namespaces!
82
+
83
+ rootfile = doc.at_xpath('//rootfile')
84
+ if rootfile.nil?
85
+ UI.error! "This is not valid EPUB file (#{CONTAINER_PATH} file does not contain any <rootfile> element)"
86
+ end
87
+
88
+ rootfile['full-path']
89
+ end
90
+
91
+ # @param [Nokogiri::XML::Document] opf
92
+ # @param [Zip::File] zip_file
93
+ #
94
+ # @return [String]
95
+ def generate_bookspec
96
+ nav_node, nav_mode = @opf.find_nav
97
+ if nav_node
98
+ nav_path = Pathname.new(@opf_path)
99
+ .dirname
100
+ .join(nav_node.href)
101
+ .to_s
102
+ nav = NavFile.new(@zip_file.read(nav_path), nav_mode)
103
+ end
104
+
105
+ BookspecGenerator.new(@opf, nav).generate_bookspec
106
+ end
107
+
108
+ def export_files
109
+ @opf.manifest_items.each_value do |item|
110
+ # ignore text files which are not in spine
111
+ text_file_extensions = %w[.xhtml .html]
112
+ extension = File.extname(item.href).downcase
113
+ if text_file_extensions.include?(extension) &&
114
+ @opf.spine_items.none? { |spine_item| spine_item.idref == item.id }
115
+ UI.puts " Skipping #{item.href} (not in spine)"
116
+ next
117
+ end
118
+
119
+ # ignore ncx file
120
+ if item.media_type == 'application/x-dtbncx+xml'
121
+ UI.puts " Skipping #{item.href} (ncx file)"
122
+ next
123
+ end
124
+
125
+ full_path = Pathname.new(@opf_path)
126
+ .dirname
127
+ .join(item.href)
128
+ .to_s
129
+
130
+ UI.puts " Exporting #{item.href} (from #{full_path})"
131
+
132
+ contents = @zip_file.read(full_path)
133
+ contents = @encryption_handler.process_file(full_path, contents) if @encryption_handler
134
+
135
+ FileUtils.mkdir_p(File.dirname(item.href))
136
+ File.write(item.href, contents)
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Epuber
4
+ class NavFile
5
+ class NavItem
6
+ # @return [String]
7
+ attr_accessor :href
8
+ # @return [String]
9
+ attr_accessor :title
10
+ # @return [Array<NavItem>]
11
+ attr_accessor :children
12
+
13
+ # @param [String] href
14
+ # @param [String] title
15
+ #
16
+ def initialize(href, title)
17
+ @href = href
18
+ @title = title
19
+ @children = []
20
+ end
21
+
22
+ # @param [String] other_href
23
+ # @param [Boolean] ignore_fragment
24
+ #
25
+ # @return [NavItem, nil]
26
+ #
27
+ def find_by_href(other_href, ignore_fragment: false)
28
+ if ignore_fragment
29
+ other_href = other_href.split('#').first
30
+ self_href = @href.split('#').first
31
+ return self if self_href == other_href
32
+ elsif @href == other_href
33
+ return self
34
+ end
35
+
36
+ @children.find { |item| item.find_by_href(other_href) }
37
+ end
38
+ end
39
+
40
+ class LandmarkItem
41
+ # @return [String]
42
+ #
43
+ attr_accessor :href
44
+
45
+ # @return [String]
46
+ #
47
+ attr_accessor :type
48
+
49
+ # @param [String] href
50
+ # @param [String] type
51
+ #
52
+ def initialize(href, type)
53
+ @href = href
54
+ @type = type
55
+ end
56
+
57
+ # @param [Nokogiri::XML::Node] node
58
+ #
59
+ # @return [LandmarkItem]
60
+ #
61
+ def self.from_node(node)
62
+ new(node['href'], node['type'])
63
+ end
64
+ end
65
+
66
+ LANDMARKS_MAP = {
67
+ 'cover' => :landmark_cover,
68
+ 'bodymatter' => :landmark_start_page,
69
+ 'copyright-page' => :landmark_copyright,
70
+ 'toc' => :landmark_toc,
71
+ }.freeze
72
+
73
+ MODE_NCX = :ncx
74
+ MODE_XHTML = :xhtml
75
+
76
+ # @return [Nokogiri::XML::Document]
77
+ #
78
+ attr_reader :document
79
+
80
+ # @return [:ncx, :xhtml]
81
+ #
82
+ attr_reader :mode
83
+
84
+ # @return [Array<NavItem>]
85
+ #
86
+ attr_reader :items
87
+
88
+ # @return [Array<LandmarkItem>, nil]
89
+ #
90
+ attr_reader :landmarks
91
+
92
+ # @param [string] document
93
+ # @param [:ncx, :xhtml] mode
94
+ #
95
+ def initialize(document, mode)
96
+ raise ArgumentError, 'mode must be :ncx or :xhtml' unless [MODE_NCX, MODE_XHTML].include?(mode)
97
+
98
+ @document = Nokogiri::XML(document)
99
+ @document.remove_namespaces!
100
+
101
+ @mode = mode
102
+ @items = _parse
103
+
104
+ @landmarks = _parse_landmarks
105
+ end
106
+
107
+ # @param [String] href
108
+ # @param [Boolean] ignore_fragment
109
+ #
110
+ # @return [NavItem, nil]
111
+ #
112
+ def find_by_href(href, ignore_fragment: false)
113
+ @items.find { |item| item.find_by_href(href, ignore_fragment: ignore_fragment) }
114
+ end
115
+
116
+ private
117
+
118
+ # @param [Nokogiri::XML::Element] li_node
119
+ #
120
+ # @return [NavItem]
121
+ #
122
+ def _parse_nav_xhtml_item(li_node)
123
+ href = li_node.at_css('a')['href'].strip
124
+ title = li_node.at_css('a').text.strip
125
+
126
+ item = NavItem.new(href, title)
127
+ item.children = li_node.css('ol > li').map { |p| _parse_nav_xhtml_item(p) }
128
+ item
129
+ end
130
+
131
+ # @param [Nokogiri::XML::Element] point_node
132
+ #
133
+ # @return [NavItem]
134
+ #
135
+ def _parse_nav_ncx_item(point_node)
136
+ href = point_node.at_css('content')['src'].strip
137
+ title = point_node.at_css('navLabel text').text.strip
138
+
139
+ item = NavItem.new(href, title)
140
+ item.children = point_node.css('> navPoint').map { |p| _parse_nav_ncx_item(p) }
141
+ item
142
+ end
143
+
144
+ # @return [Array<NavItem>]
145
+ #
146
+ def _parse
147
+ if @mode == MODE_XHTML
148
+ @document.css('nav[type="toc"] > ol > li')
149
+ .map { |point| _parse_nav_xhtml_item(point) }
150
+ elsif @mode == MODE_NCX
151
+ @document.css('navMap > navPoint')
152
+ .map { |point| _parse_nav_ncx_item(point) }
153
+ end
154
+ end
155
+
156
+ def _parse_landmarks
157
+ return nil if @mode != MODE_XHTML
158
+
159
+ @document.css('nav[type="landmarks"] > ol > li > a')
160
+ .map { |node| LandmarkItem.from_node(node) }
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Epuber
4
+ class OpfFile
5
+ class ManifestItem
6
+ # @return [String]
7
+ #
8
+ attr_accessor :id
9
+
10
+ # @return [String]
11
+ #
12
+ attr_accessor :href
13
+
14
+ # @return [String]
15
+ #
16
+ attr_accessor :media_type
17
+
18
+ # @return [String, nil]
19
+ #
20
+ attr_accessor :properties
21
+
22
+ def initialize(id, href, media_type, properties)
23
+ @id = id
24
+ @href = href
25
+ @media_type = media_type
26
+ @properties = properties
27
+ end
28
+
29
+ # @param [ManifestItem] other
30
+ #
31
+ # @return [Boolean]
32
+ #
33
+ def ==(other)
34
+ @id == other.id
35
+ end
36
+
37
+ # @param [Nokogiri::XML::Node] node
38
+ #
39
+ # @return [ManifestItem]
40
+ #
41
+ def self.from_node(node)
42
+ new(node['id'], node['href'], node['media-type'], node['properties'])
43
+ end
44
+ end
45
+
46
+ class SpineItem
47
+ # @return [String]
48
+ #
49
+ attr_accessor :idref
50
+
51
+ # @return [String]
52
+ #
53
+ attr_accessor :linear
54
+
55
+ def initialize(idref, linear)
56
+ @idref = idref
57
+ @linear = linear
58
+ end
59
+
60
+ # @param [SpineItem] other
61
+ #
62
+ # @return [Boolean]
63
+ #
64
+ def ==(other)
65
+ @idref == other.idref
66
+ end
67
+
68
+ # @param [Nokogiri::XML::Node] node
69
+ #
70
+ # @return [SpineItem]
71
+ #
72
+ def self.from_node(node)
73
+ new(node['idref'], node['linear'])
74
+ end
75
+ end
76
+
77
+ class GuideItem
78
+ # @return [String]
79
+ #
80
+ attr_accessor :type
81
+
82
+ # @return [String]
83
+ #
84
+ attr_accessor :href
85
+
86
+ def initialize(type, href)
87
+ @type = type
88
+ @href = href
89
+ end
90
+
91
+ # @param [Nokogiri::XML::Node] node
92
+ #
93
+ # @return [GuideItem]
94
+ #
95
+ def self.from_node(node)
96
+ new(node['type'], node['href'])
97
+ end
98
+ end
99
+
100
+ # reversed map of generator's map
101
+ LANDMARKS_MAP = Compiler::OPFGenerator::LANDMARKS_MAP.map { |k, v| [v, k] }
102
+ .to_h
103
+ .freeze
104
+
105
+ # @return [Nokogiri::XML::Document]
106
+ #
107
+ attr_reader :document
108
+
109
+ # @return [Nokogiri::XML::Node, nil]
110
+ #
111
+ attr_reader :package, :metadata, :manifest, :spine
112
+
113
+ # @return [Hash<String, ManifestItem>]
114
+ #
115
+ attr_reader :manifest_items
116
+
117
+ # @return [Array<SpineItem>]
118
+ #
119
+ attr_reader :spine_items
120
+
121
+ # @return [Array<GuideItem>]
122
+ #
123
+ attr_reader :guide_items
124
+
125
+ # @param [String] document
126
+ def initialize(xml)
127
+ @document = Nokogiri::XML(xml)
128
+ @document.remove_namespaces!
129
+
130
+ @package = @document.at_css('package')
131
+ @metadata = @document.at_css('package metadata')
132
+ @manifest = @document.at_css('package manifest')
133
+ @spine = @document.at_css('package spine')
134
+
135
+ @manifest_items = @document.css('package manifest item')
136
+ .map { |node| ManifestItem.from_node(node) }
137
+ .map { |item| [item.id, item] }
138
+ .to_h
139
+ @spine_items = @document.css('package spine itemref')
140
+ .map { |node| SpineItem.from_node(node) }
141
+ @guide_items = @document.css('package guide reference')
142
+ .map { |node| GuideItem.from_node(node) }
143
+ end
144
+
145
+ # Find nav file in EPUB (both EPUB 2 and EPUB 3). Returns array with nav and type of nav (:xhtml or :ncx).
146
+ #
147
+ # @return [Array<ManifestItem, [:ncx, :xhtml]>, nil] nav, ncx
148
+ #
149
+ def find_nav
150
+ nav = @manifest_items.find { |_, item| item.properties == 'nav' }&.last
151
+ return [nav, NavFile::MODE_XHTML] if nav
152
+
153
+ ncx_id = @spine['toc'] if @spine
154
+ ncx = manifest_file_by_id(ncx_id) if ncx_id
155
+ return [ncx, NavFile::MODE_NCX] if ncx
156
+
157
+ nil
158
+ end
159
+
160
+ # Returns main unique identifier of this EPUB
161
+ #
162
+ # @return [String, nil]
163
+ #
164
+ def raw_unique_identifier
165
+ id = @package['unique-identifier']
166
+ return unless id
167
+
168
+ @metadata.at_css(%(identifier[id="#{id}"]))&.text
169
+ end
170
+
171
+ # Return all identifiers from EPUB metadata
172
+ #
173
+ # @return [Array<Nokogiri::XML::Node>]
174
+ #
175
+ def identifiers
176
+ @metadata.css('identifier')
177
+ end
178
+
179
+ # Find meta refines in EPUB 3 metadata
180
+ #
181
+ # @param [String] id
182
+ # @param [String] property
183
+ #
184
+ # @return [String, nil]
185
+ #
186
+ def find_refines(id, property)
187
+ @metadata.at_css(%(meta[refines="##{id}"][property="#{property}"]))&.text
188
+ end
189
+
190
+ # Find file in <manifest> by id. Throws exception when not found.
191
+ #
192
+ # @param [String] id
193
+ #
194
+ # @return [ManifestItem]
195
+ #
196
+ def manifest_file_by_id(id)
197
+ item = @manifest_items[id]
198
+ raise "Manifest item with id #{id.inspect} not found" unless item
199
+
200
+ item
201
+ end
202
+
203
+ # Find file in <manifest> by href. Throws exception when not found.
204
+ #
205
+ # @param [String] href
206
+ #
207
+ # @return [ManifestItem]
208
+ #
209
+ def manifest_file_by_href(href)
210
+ # remove anchor
211
+ href = href.sub(/#.*$/, '')
212
+
213
+ item = @manifest_items.find { |_, i| i.href == href }&.last
214
+ raise "Manifest item with href #{href.inspect} not found" unless item
215
+
216
+ item
217
+ end
218
+ end
219
+ end
data/lib/epuber/plugin.rb CHANGED
@@ -66,7 +66,7 @@ module Epuber
66
66
  #
67
67
  attr_reader :files
68
68
 
69
- # @param path [String]
69
+ # @param [String] path
70
70
  #
71
71
  def initialize(path)
72
72
  @path = path