epub-parser-io 0.1.6a
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.
- data/.gemtest +0 -0
- data/.gitignore +12 -0
- data/.gitmodules +3 -0
- data/.travis.yml +4 -0
- data/.yardopts +10 -0
- data/CHANGELOG.markdown +61 -0
- data/Gemfile +2 -0
- data/MIT-LICENSE +7 -0
- data/README.markdown +174 -0
- data/Rakefile +68 -0
- data/bin/epub-open +25 -0
- data/bin/epubinfo +64 -0
- data/docs/EpubOpen.markdown +43 -0
- data/docs/Epubinfo.markdown +37 -0
- data/docs/FixedLayout.markdown +96 -0
- data/docs/Home.markdown +128 -0
- data/docs/Item.markdown +80 -0
- data/docs/Navigation.markdown +58 -0
- data/docs/Publication.markdown +54 -0
- data/epub-parser.gemspec +49 -0
- data/features/epubinfo.feature +6 -0
- data/features/step_definitions/epubinfo_steps.rb +5 -0
- data/features/support/env.rb +1 -0
- data/lib/epub/book/features.rb +85 -0
- data/lib/epub/book.rb +7 -0
- data/lib/epub/constants.rb +48 -0
- data/lib/epub/content_document/navigation.rb +104 -0
- data/lib/epub/content_document/xhtml.rb +41 -0
- data/lib/epub/content_document.rb +2 -0
- data/lib/epub/inspector.rb +45 -0
- data/lib/epub/ocf/container.rb +28 -0
- data/lib/epub/ocf/encryption.rb +7 -0
- data/lib/epub/ocf/manifest.rb +6 -0
- data/lib/epub/ocf/metadata.rb +6 -0
- data/lib/epub/ocf/rights.rb +6 -0
- data/lib/epub/ocf/signatures.rb +6 -0
- data/lib/epub/ocf.rb +8 -0
- data/lib/epub/parser/content_document.rb +111 -0
- data/lib/epub/parser/ocf.rb +73 -0
- data/lib/epub/parser/publication.rb +200 -0
- data/lib/epub/parser/utils.rb +20 -0
- data/lib/epub/parser/version.rb +5 -0
- data/lib/epub/parser.rb +103 -0
- data/lib/epub/publication/fixed_layout.rb +208 -0
- data/lib/epub/publication/package/bindings.rb +31 -0
- data/lib/epub/publication/package/guide.rb +51 -0
- data/lib/epub/publication/package/manifest.rb +180 -0
- data/lib/epub/publication/package/metadata.rb +170 -0
- data/lib/epub/publication/package/spine.rb +106 -0
- data/lib/epub/publication/package.rb +68 -0
- data/lib/epub/publication.rb +2 -0
- data/lib/epub.rb +14 -0
- data/man/epubinfo.1.ronn +19 -0
- data/schemas/epub-nav-30.rnc +10 -0
- data/schemas/epub-nav-30.sch +72 -0
- data/schemas/epub-xhtml-30.sch +377 -0
- data/schemas/ocf-container-30.rnc +16 -0
- data/test/fixtures/book/META-INF/container.xml +6 -0
- data/test/fixtures/book/OPS/%E6%97%A5%E6%9C%AC%E8%AA%9E.xhtml +10 -0
- data/test/fixtures/book/OPS/case-sensitive.xhtml +9 -0
- data/test/fixtures/book/OPS/containing space.xhtml +10 -0
- data/test/fixtures/book/OPS/containing%20space.xhtml +10 -0
- data/test/fixtures/book/OPS/nav.xhtml +28 -0
- data/test/fixtures/book/OPS//343/203/253/343/203/274/343/203/210/343/203/225/343/202/241/343/202/244/343/203/253.opf +119 -0
- data/test/fixtures/book/OPS//346/227/245/346/234/254/350/252/236.xhtml +10 -0
- data/test/fixtures/book/mimetype +1 -0
- data/test/helper.rb +9 -0
- data/test/test_content_document.rb +92 -0
- data/test/test_epub.rb +21 -0
- data/test/test_fixed_layout.rb +257 -0
- data/test/test_inspect.rb +121 -0
- data/test/test_parser.rb +60 -0
- data/test/test_parser_content_document.rb +36 -0
- data/test/test_parser_fixed_layout.rb +16 -0
- data/test/test_parser_ocf.rb +38 -0
- data/test/test_parser_publication.rb +247 -0
- data/test/test_publication.rb +324 -0
- metadata +445 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
module EPUB
|
2
|
+
class Parser
|
3
|
+
module Utils
|
4
|
+
# Extract the value of attribute of element
|
5
|
+
#
|
6
|
+
# @todo Refinement Nokogiri::XML::Node instead of use this method after Ruby 2.0 becomes popular
|
7
|
+
#
|
8
|
+
# @param [Nokogiri::XML::Element] element
|
9
|
+
# @param [String] name name of attribute excluding namespace prefix
|
10
|
+
# @param [String, nil] prefix XML namespace prefix in {EPUB::Constants::NAMESPACES} keys
|
11
|
+
# @return [String] value of attribute when the attribute exists
|
12
|
+
# @return nil when the attribute doesn't exist
|
13
|
+
def extract_attribute(element, name, prefix=nil)
|
14
|
+
attr = element.attribute_with_ns(name, EPUB::NAMESPACES[prefix])
|
15
|
+
attr.nil? ? nil : attr.value
|
16
|
+
end
|
17
|
+
module_function :extract_attribute
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/epub/parser.rb
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'epub'
|
2
|
+
require 'epub/constants'
|
3
|
+
require 'zipruby'
|
4
|
+
require 'nokogiri'
|
5
|
+
require 'tempfile'
|
6
|
+
|
7
|
+
module EPUB
|
8
|
+
class Parser
|
9
|
+
class << self
|
10
|
+
# Parse an EPUB file
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# EPUB::Parser.parse('path/to/book.epub') # => EPUB::Book object
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# class MyBook
|
17
|
+
# include EPUB
|
18
|
+
# end
|
19
|
+
# book = MyBook.new
|
20
|
+
# parsed_book = EPUB::Parser.parse('path/to/book.epub', :book => book) # => #<MyBook:0x000000019760e8 @epub_file=..>
|
21
|
+
# parsed_book.equal? book # => true
|
22
|
+
#
|
23
|
+
# @example
|
24
|
+
# book = EPUB::Parser.parse('path/to/book.epub', :class => MyBook) # => #<MyBook:0x000000019b0568 @epub_file=...>
|
25
|
+
# book.instance_of? MyBook # => true
|
26
|
+
#
|
27
|
+
# @param [String] filepath
|
28
|
+
# @param [Hash] options the type of return is specified by this argument.
|
29
|
+
# If no options, returns {EPUB::Book} object.
|
30
|
+
# For details of options, see below.
|
31
|
+
# @option options [EPUB] :book instance of class which includes {EPUB} module
|
32
|
+
# @option options [Class] :class class which includes {EPUB} module
|
33
|
+
# @return [EPUB] object which is an instance of class including {EPUB} module.
|
34
|
+
# When option :book passed, returns the same object whose attributes about EPUB are set.
|
35
|
+
# When option :class passed, returns the instance of the class.
|
36
|
+
# Otherwise returns {EPUB::Book} object.
|
37
|
+
def parse(filepath, options = {})
|
38
|
+
new(filepath, options).parse
|
39
|
+
end
|
40
|
+
|
41
|
+
def parse_io(io_stream, options = {})
|
42
|
+
new(io_stream, options.merge(io: true)).parse_io
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def initialize(datasource, options = {})
|
47
|
+
if options[:io]
|
48
|
+
raise "IO source not readable" unless datasource.respond_to?(:read)
|
49
|
+
|
50
|
+
@io_stream = datasource
|
51
|
+
@book = create_book options
|
52
|
+
file = Tempfile.new('epub_string')
|
53
|
+
file.write(@io_stream)
|
54
|
+
@filepath = file.path
|
55
|
+
@book.epub_file = @filepath
|
56
|
+
else
|
57
|
+
raise "File #{datasource} not readable" unless File.readable_real? datasource
|
58
|
+
|
59
|
+
@filepath = File.realpath datasource
|
60
|
+
@book = create_book options
|
61
|
+
@book.epub_file = @filepath
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def parse
|
66
|
+
Zip::Archive.open @filepath do |zip|
|
67
|
+
@book.ocf = OCF.parse(zip)
|
68
|
+
@book.package = Publication.parse(zip, @book.ocf.container.rootfile.full_path.to_s)
|
69
|
+
end
|
70
|
+
|
71
|
+
@book
|
72
|
+
end
|
73
|
+
|
74
|
+
def parse_io # unnecessary, but desirable maybe?
|
75
|
+
Zip::Archive.open_buffer @io_stream do |zip|
|
76
|
+
@book.ocf = OCF.parse(zip)
|
77
|
+
@book.package = Publication.parse(zip, @book.ocf.container.rootfile.full_path.to_s)
|
78
|
+
end
|
79
|
+
|
80
|
+
@book
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def create_book(params)
|
86
|
+
case
|
87
|
+
when params[:book]
|
88
|
+
params[:book]
|
89
|
+
when params[:class]
|
90
|
+
params[:class].new
|
91
|
+
else
|
92
|
+
require 'epub/book'
|
93
|
+
Book.new
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
require 'epub/parser/version'
|
100
|
+
require 'epub/parser/utils'
|
101
|
+
require 'epub/parser/ocf'
|
102
|
+
require 'epub/parser/publication'
|
103
|
+
require 'epub/parser/content_document'
|
@@ -0,0 +1,208 @@
|
|
1
|
+
module EPUB
|
2
|
+
module Publication
|
3
|
+
module FixedLayout
|
4
|
+
PREFIX_KEY = 'rendition'.freeze
|
5
|
+
PREFIX_VALUE = 'http://www.idpf.org/vocab/rendition/#'.freeze
|
6
|
+
|
7
|
+
RENDITION_PROPERTIES = {
|
8
|
+
'layout' => ['reflowable'.freeze, 'pre-paginated'.freeze].freeze,
|
9
|
+
'orientation' => ['auto'.freeze, 'landscape'.freeze, 'portrait'.freeze].freeze,
|
10
|
+
'spread' => ['auto'.freeze, 'none'.freeze, 'landscape'.freeze, 'portrait'.freeze, 'both'.freeze].freeze
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
class UnsupportedRenditionValue < StandardError; end
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def included(package_class)
|
17
|
+
[
|
18
|
+
[Package, PackageMixin],
|
19
|
+
[Package::Metadata, MetadataMixin],
|
20
|
+
[Package::Spine::Itemref, ItemrefMixin],
|
21
|
+
[Package::Manifest::Item, ItemMixin],
|
22
|
+
[ContentDocument::XHTML, ContentDocumentMixin],
|
23
|
+
].each do |(base, mixin)|
|
24
|
+
base.__send__ :include, mixin
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module Rendition
|
30
|
+
# @note Call after defining #rendition_xxx and #renditionn_xxx=
|
31
|
+
def def_rendition_methods
|
32
|
+
RENDITION_PROPERTIES.each_key do |property|
|
33
|
+
alias_method property, "rendition_#{property}"
|
34
|
+
alias_method "#{property}=", "rendition_#{property}="
|
35
|
+
end
|
36
|
+
def_rendition_layout_methods
|
37
|
+
end
|
38
|
+
|
39
|
+
def def_rendition_layout_methods
|
40
|
+
property = 'layout'
|
41
|
+
RENDITION_PROPERTIES[property].each do |value|
|
42
|
+
method_name_base = value.gsub('-', '_')
|
43
|
+
writer_name = "#{method_name_base}="
|
44
|
+
define_method writer_name do |new_value|
|
45
|
+
new_prop = new_value ? value : values.find {|l| l != value}
|
46
|
+
__send__ "rendition_#{property}=", new_prop
|
47
|
+
end
|
48
|
+
|
49
|
+
maker_name = "make_#{method_name_base}"
|
50
|
+
define_method maker_name do
|
51
|
+
__send__ "rendition_#{property}=", value
|
52
|
+
end
|
53
|
+
destructive_method_name = "#{method_name_base}!"
|
54
|
+
alias_method destructive_method_name, maker_name
|
55
|
+
|
56
|
+
predicate_name = "#{method_name_base}?"
|
57
|
+
define_method predicate_name do
|
58
|
+
__send__("rendition_#{property}") == value
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
module PackageMixin
|
65
|
+
# @return [true, false]
|
66
|
+
def using_fixed_layout
|
67
|
+
prefix.has_key? PREFIX_KEY and
|
68
|
+
prefix[PREFIX_KEY] == PREFIX_VALUE
|
69
|
+
end
|
70
|
+
alias using_fixed_layout? using_fixed_layout
|
71
|
+
|
72
|
+
# @param using_fixed_layout [true, false]
|
73
|
+
def using_fixed_layout=(using_fixed_layout)
|
74
|
+
if using_fixed_layout
|
75
|
+
prefix[PREFIX_KEY] = PREFIX_VALUE
|
76
|
+
else
|
77
|
+
prefix.delete PREFIX_KEY
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
module MetadataMixin
|
83
|
+
extend Rendition
|
84
|
+
|
85
|
+
RENDITION_PROPERTIES.each_pair do |property, values|
|
86
|
+
define_method "rendition_#{property}" do
|
87
|
+
meta = metas.find {|m| m.property == "rendition:#{property}"}
|
88
|
+
meta ? meta.content : values.first
|
89
|
+
end
|
90
|
+
|
91
|
+
define_method "rendition_#{property}=" do |new_value|
|
92
|
+
raise UnsupportedRenditionValue, new_value unless values.include? new_value
|
93
|
+
|
94
|
+
prefixed_property = "rendition:#{property}"
|
95
|
+
values_to_be_deleted = values - [new_value]
|
96
|
+
metas.delete_if {|meta| meta.property == prefixed_property && values_to_be_deleted.include?(meta.content)}
|
97
|
+
unless metas.any? {|meta| meta.property == prefixed_property && meta.content == new_value}
|
98
|
+
meta = Package::Metadata::Meta.new
|
99
|
+
meta.property = prefixed_property
|
100
|
+
meta.content = new_value
|
101
|
+
metas << meta
|
102
|
+
end
|
103
|
+
new_value
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def_rendition_methods
|
108
|
+
end
|
109
|
+
|
110
|
+
module ItemrefMixin
|
111
|
+
extend Rendition
|
112
|
+
|
113
|
+
PAGE_SPREAD_PROPERTY = 'center'
|
114
|
+
PAGE_SPREAD_PREFIX = 'rendition:page-spread-'
|
115
|
+
|
116
|
+
class << self
|
117
|
+
# @todo Define using Module#prepend after Ruby 2.0 will become popular
|
118
|
+
def included(base)
|
119
|
+
return if base.instance_methods.include? :page_spread_without_fixed_layout
|
120
|
+
base.__send__ :alias_method, :page_spread_without_fixed_layout, :page_spread
|
121
|
+
base.__send__ :alias_method, :page_spread_writer_without_fixed_layout, :page_spread=
|
122
|
+
|
123
|
+
prefixed_page_spread_property = "#{PAGE_SPREAD_PREFIX}#{PAGE_SPREAD_PROPERTY}"
|
124
|
+
base.__send__ :define_method, :page_spread do
|
125
|
+
property = page_spread_without_fixed_layout
|
126
|
+
return property if property
|
127
|
+
properties.include?(prefixed_page_spread_property) ? PAGE_SPREAD_PROPERTY : nil
|
128
|
+
end
|
129
|
+
|
130
|
+
base.__send__ :define_method, :page_spread= do |new_value|
|
131
|
+
if new_value == PAGE_SPREAD_PROPERTY
|
132
|
+
page_spread_writer_without_fixed_layout nil
|
133
|
+
properties << prefixed_page_spread_property
|
134
|
+
else
|
135
|
+
page_spread_writer_without_fixed_layout new_value
|
136
|
+
end
|
137
|
+
new_value
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
RENDITION_PROPERTIES.each do |property, values|
|
143
|
+
rendition_property_prefix = "rendition:#{property}-"
|
144
|
+
|
145
|
+
reader_name = "rendition_#{property}"
|
146
|
+
define_method reader_name do
|
147
|
+
prop_value = properties.find {|prop| prop.start_with? rendition_property_prefix}
|
148
|
+
prop_value ? prop_value.gsub(/\A#{Regexp.escape(rendition_property_prefix)}/, '') :
|
149
|
+
spine.package.metadata.__send__(reader_name)
|
150
|
+
end
|
151
|
+
|
152
|
+
writer_name = "#{reader_name}="
|
153
|
+
define_method writer_name do |new_value|
|
154
|
+
if new_value.nil?
|
155
|
+
properties.delete_if {|prop| prop.start_with? rendition_property_prefix}
|
156
|
+
return new_value
|
157
|
+
end
|
158
|
+
|
159
|
+
raise UnsupportedRenditionValue, new_value unless values.include? new_value
|
160
|
+
|
161
|
+
values_to_be_deleted = (values - [new_value]).map {|value| "#{rendition_property_prefix}#{value}"}
|
162
|
+
properties.delete_if {|prop| values_to_be_deleted.include? prop}
|
163
|
+
new_property = "#{rendition_property_prefix}#{new_value}"
|
164
|
+
properties << new_property unless properties.include? new_property
|
165
|
+
new_value
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def_rendition_methods
|
170
|
+
end
|
171
|
+
|
172
|
+
module ItemMixin
|
173
|
+
extend Rendition
|
174
|
+
|
175
|
+
RENDITION_PROPERTIES.each_key do |property|
|
176
|
+
define_method "rendition_#{property}" do
|
177
|
+
itemref.__send__ property
|
178
|
+
end
|
179
|
+
|
180
|
+
writer_name = "rendition_#{property}="
|
181
|
+
define_method writer_name do |value|
|
182
|
+
itemref.__send__ writer_name, value
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def_rendition_methods
|
187
|
+
end
|
188
|
+
|
189
|
+
module ContentDocumentMixin
|
190
|
+
extend Rendition
|
191
|
+
|
192
|
+
RENDITION_PROPERTIES.each_key do |property|
|
193
|
+
reader_name = "rendition_#{property}"
|
194
|
+
define_method reader_name do
|
195
|
+
item.__send__ reader_name
|
196
|
+
end
|
197
|
+
|
198
|
+
writer_name = "rendition_#{property}="
|
199
|
+
define_method writer_name do |value|
|
200
|
+
item.__send__ writer_name, value
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def_rendition_methods
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module EPUB
|
2
|
+
module Publication
|
3
|
+
class Package
|
4
|
+
class Bindings
|
5
|
+
include Inspector::PublicationModel
|
6
|
+
attr_accessor :package
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@media_types = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def <<(media_type)
|
13
|
+
@media_types[media_type.media_type] = media_type
|
14
|
+
end
|
15
|
+
|
16
|
+
def [](media_type)
|
17
|
+
_, mt = @media_types.detect {|key, _| key == media_type}
|
18
|
+
mt
|
19
|
+
end
|
20
|
+
|
21
|
+
def media_types
|
22
|
+
@media_types.values
|
23
|
+
end
|
24
|
+
|
25
|
+
class MediaType
|
26
|
+
attr_accessor :media_type, :handler
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'enumerabler'
|
2
|
+
|
3
|
+
module EPUB
|
4
|
+
module Publication
|
5
|
+
class Package
|
6
|
+
class Guide
|
7
|
+
include Inspector::PublicationModel
|
8
|
+
attr_accessor :package, :references
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
Reference::TYPES.each do |type|
|
12
|
+
variable_name = '@' + type.gsub('-', '_')
|
13
|
+
instance_variable_set variable_name, nil
|
14
|
+
end
|
15
|
+
@references = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def <<(reference)
|
19
|
+
reference.guide = self
|
20
|
+
references << reference
|
21
|
+
end
|
22
|
+
|
23
|
+
class Reference
|
24
|
+
TYPES = %w[cover title-page toc index glossary acknowledgements bibliography colophon copyright-page dedication epigraph foreword loi lot notes preface text]
|
25
|
+
attr_accessor :guide,
|
26
|
+
:type, :title, :href
|
27
|
+
|
28
|
+
def item
|
29
|
+
return @item if @item
|
30
|
+
|
31
|
+
request_uri = href.request_uri
|
32
|
+
@item = @guide.package.manifest.items.selector do |item|
|
33
|
+
item.href.request_uri == request_uri
|
34
|
+
end.first
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
Reference::TYPES.each do |type|
|
39
|
+
method_name = type.gsub('-', '_')
|
40
|
+
define_method method_name do
|
41
|
+
var = instance_variable_get "@#{method_name}"
|
42
|
+
return var if var
|
43
|
+
|
44
|
+
var = references.selector {|ref| ref.type == type}.first
|
45
|
+
instance_variable_set "@#{method_name}", var
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'enumerabler'
|
3
|
+
require 'epub/constants'
|
4
|
+
require 'epub/parser/content_document'
|
5
|
+
|
6
|
+
module EPUB
|
7
|
+
module Publication
|
8
|
+
class Package
|
9
|
+
class Manifest
|
10
|
+
include Inspector::PublicationModel
|
11
|
+
|
12
|
+
attr_accessor :package,
|
13
|
+
:id
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@items = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return self
|
20
|
+
def <<(item)
|
21
|
+
item.manifest = self
|
22
|
+
@items[item.id] = item
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def navs
|
27
|
+
items.selector(&:nav?)
|
28
|
+
end
|
29
|
+
|
30
|
+
def nav
|
31
|
+
navs.first
|
32
|
+
end
|
33
|
+
|
34
|
+
def cover_image
|
35
|
+
items.selector(&:cover_image?).first
|
36
|
+
end
|
37
|
+
|
38
|
+
def each_item
|
39
|
+
@items.each_value do |item|
|
40
|
+
yield item
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def items
|
45
|
+
@items.values
|
46
|
+
end
|
47
|
+
|
48
|
+
def [](item_id)
|
49
|
+
@items[item_id]
|
50
|
+
end
|
51
|
+
|
52
|
+
class Item
|
53
|
+
include Inspector
|
54
|
+
|
55
|
+
# @!attribute [rw] manifest
|
56
|
+
# @return [Manifest] Returns the value of manifest
|
57
|
+
# @!attribute [rw] id
|
58
|
+
# @return [String] Returns the value of id
|
59
|
+
# @!attribute [rw] href
|
60
|
+
# @return [Addressable::URI] Returns the value of href,
|
61
|
+
# which is relative IRI from rootfile(OPF file)
|
62
|
+
# @!attribute [rw] media_type
|
63
|
+
# @return [String] Returns the value of media_type
|
64
|
+
# @!attribute [rw] properties
|
65
|
+
# @return [Set<String>] Returns the value of properties
|
66
|
+
# @!attribute [rw] media_overlay
|
67
|
+
# @return [String] Returns the value of media_overlay
|
68
|
+
# @!attribute [rw] fallback
|
69
|
+
# @return [Item] Returns the value of attribute fallback
|
70
|
+
attr_accessor :manifest,
|
71
|
+
:id, :href, :media_type, :fallback, :media_overlay
|
72
|
+
attr_reader :properties
|
73
|
+
|
74
|
+
def initialize
|
75
|
+
@properties = Set.new
|
76
|
+
end
|
77
|
+
|
78
|
+
def properties=(props)
|
79
|
+
@properties = props.kind_of?(Set) ? props : Set.new(props)
|
80
|
+
end
|
81
|
+
|
82
|
+
# @todo Handle circular fallback chain
|
83
|
+
def fallback_chain
|
84
|
+
@fallback_chain ||= traverse_fallback_chain([])
|
85
|
+
end
|
86
|
+
|
87
|
+
# full path in archive
|
88
|
+
def entry_name
|
89
|
+
rootfile = manifest.package.book.ocf.container.rootfile.full_path
|
90
|
+
Addressable::URI.unescape(rootfile + href.normalize.request_uri)
|
91
|
+
end
|
92
|
+
|
93
|
+
def read
|
94
|
+
Zip::Archive.open(manifest.package.book.epub_file) {|zip|
|
95
|
+
zip.fopen(entry_name).read
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
def xhtml?
|
100
|
+
media_type == 'application/xhtml+xml'
|
101
|
+
end
|
102
|
+
|
103
|
+
def nav?
|
104
|
+
properties.include? 'nav'
|
105
|
+
end
|
106
|
+
|
107
|
+
def cover_image?
|
108
|
+
properties.include? 'cover-image'
|
109
|
+
end
|
110
|
+
|
111
|
+
# @todo Handle circular fallback chain
|
112
|
+
def use_fallback_chain(options = {})
|
113
|
+
supported = EPUB::MediaType::CORE
|
114
|
+
if ad = options[:supported]
|
115
|
+
supported = supported | (ad.respond_to?(:to_ary) ? ad : [ad])
|
116
|
+
end
|
117
|
+
if del = options[:unsupported]
|
118
|
+
supported = supported - (del.respond_to?(:to_ary) ? del : [del])
|
119
|
+
end
|
120
|
+
|
121
|
+
return yield self if supported.include? media_type
|
122
|
+
if (bindings = manifest.package.bindings) && (binding_media_type = bindings[media_type])
|
123
|
+
return yield binding_media_type.handler
|
124
|
+
end
|
125
|
+
return fallback.use_fallback_chain(options) {|fb| yield fb} if fallback
|
126
|
+
raise EPUB::MediaType::UnsupportedMediaType
|
127
|
+
end
|
128
|
+
|
129
|
+
def content_document
|
130
|
+
return nil unless %w[application/xhtml+xml image/svg+xml].include? media_type
|
131
|
+
@content_document ||= Parser::ContentDocument.new(self).parse
|
132
|
+
end
|
133
|
+
|
134
|
+
# @return [Package::Spine::Itemref]
|
135
|
+
# @return nil when no Itemref refers this Item
|
136
|
+
def itemref
|
137
|
+
manifest.package.spine.itemrefs.find {|itemref| itemref.idref == id}
|
138
|
+
end
|
139
|
+
|
140
|
+
# @param iri [Addressable::URI] relative iri
|
141
|
+
# @return [Item]
|
142
|
+
# @return [nil] when item not found
|
143
|
+
# @raise ArgumentError when +iri+ is not relative
|
144
|
+
# @raise ArgumentError when +iri+ starts with "/"(slash)
|
145
|
+
# @note Algorithm stolen form Rack::Utils#clean_path_info
|
146
|
+
def find_item_by_relative_iri(iri)
|
147
|
+
raise ArgumentError, "Not relative: #{iri.inspect}" unless iri.relative?
|
148
|
+
raise ArgumentError, "Start with slash: #{iri.inspect}" if iri.to_s.start_with? Addressable::URI::SLASH
|
149
|
+
target_href = href + iri
|
150
|
+
segments = target_href.to_s.split(Addressable::URI::SLASH)
|
151
|
+
clean_segments = []
|
152
|
+
segments.each do |segment|
|
153
|
+
next if segment.empty? || segment == '.'
|
154
|
+
segment == '..' ? clean_segments.pop : clean_segments << segment
|
155
|
+
end
|
156
|
+
target_iri = Addressable::URI.parse(clean_segments.join(Addressable::URI::SLASH))
|
157
|
+
manifest.items.find { |item| item.href == target_iri}
|
158
|
+
end
|
159
|
+
|
160
|
+
def inspect
|
161
|
+
"#<%{class}:%{object_id} %{manifest} %{attributes}>" % {
|
162
|
+
:class => self.class,
|
163
|
+
:object_id => inspect_object_id,
|
164
|
+
:manifest => "@manifest=#{@manifest.inspect_simply}",
|
165
|
+
:attributes => inspect_instance_variables(exclude: [:@manifest])
|
166
|
+
}
|
167
|
+
end
|
168
|
+
|
169
|
+
protected
|
170
|
+
|
171
|
+
def traverse_fallback_chain(chain)
|
172
|
+
chain << self
|
173
|
+
return chain unless fallback
|
174
|
+
fallback.traverse_fallback_chain(chain)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|