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.
- 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 +84 -0
- data/lib/epuber/compiler/file_types/source_file.rb +31 -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 +7 -25
- 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
|