epuber 0.8.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +9 -0
- data/LICENSE.txt +1 -1
- data/README.md +2 -2
- data/epuber.gemspec +3 -6
- data/lib/epuber/book/contributor.rb +10 -6
- data/lib/epuber/book/file_request.rb +2 -2
- data/lib/epuber/book/target.rb +10 -10
- data/lib/epuber/book.rb +2 -2
- data/lib/epuber/checker/text_checker.rb +14 -6
- data/lib/epuber/checker_transformer_base.rb +1 -1
- data/lib/epuber/command/build.rb +6 -1
- data/lib/epuber/command/from_file.rb +39 -0
- data/lib/epuber/command/init.rb +11 -9
- data/lib/epuber/command/server.rb +1 -1
- data/lib/epuber/command.rb +1 -0
- data/lib/epuber/compiler/file_database.rb +2 -2
- data/lib/epuber/compiler/file_finders/abstract.rb +3 -3
- data/lib/epuber/compiler/file_resolver.rb +3 -2
- data/lib/epuber/compiler/file_types/abstract_file.rb +1 -3
- data/lib/epuber/compiler/file_types/bade_file.rb +9 -9
- data/lib/epuber/compiler/file_types/css_file.rb +103 -0
- data/lib/epuber/compiler/file_types/stylus_file.rb +4 -3
- data/lib/epuber/compiler/nav_generator.rb +5 -5
- data/lib/epuber/compiler/opf_generator.rb +4 -4
- data/lib/epuber/compiler/xhtml_processor.rb +2 -1
- data/lib/epuber/compiler.rb +12 -7
- data/lib/epuber/config.rb +3 -3
- data/lib/epuber/dsl/attribute.rb +1 -1
- data/lib/epuber/dsl/attribute_support.rb +4 -4
- data/lib/epuber/dsl/object.rb +2 -2
- data/lib/epuber/from_file/bookspec_generator.rb +371 -0
- data/lib/epuber/from_file/encryption_handler.rb +146 -0
- data/lib/epuber/from_file/from_file_executor.rb +140 -0
- data/lib/epuber/from_file/nav_file.rb +163 -0
- data/lib/epuber/from_file/opf_file.rb +219 -0
- data/lib/epuber/plugin.rb +1 -1
- data/lib/epuber/server.rb +17 -17
- data/lib/epuber/transformer/book_transformer.rb +108 -0
- data/lib/epuber/transformer.rb +2 -0
- data/lib/epuber/user_interface.rb +2 -2
- data/lib/epuber/vendor/ruby_templater.rb +3 -3
- data/lib/epuber/vendor/version.rb +3 -3
- data/lib/epuber/version.rb +1 -1
- 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
|