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.
Files changed (78) hide show
  1. data/.gemtest +0 -0
  2. data/.gitignore +12 -0
  3. data/.gitmodules +3 -0
  4. data/.travis.yml +4 -0
  5. data/.yardopts +10 -0
  6. data/CHANGELOG.markdown +61 -0
  7. data/Gemfile +2 -0
  8. data/MIT-LICENSE +7 -0
  9. data/README.markdown +174 -0
  10. data/Rakefile +68 -0
  11. data/bin/epub-open +25 -0
  12. data/bin/epubinfo +64 -0
  13. data/docs/EpubOpen.markdown +43 -0
  14. data/docs/Epubinfo.markdown +37 -0
  15. data/docs/FixedLayout.markdown +96 -0
  16. data/docs/Home.markdown +128 -0
  17. data/docs/Item.markdown +80 -0
  18. data/docs/Navigation.markdown +58 -0
  19. data/docs/Publication.markdown +54 -0
  20. data/epub-parser.gemspec +49 -0
  21. data/features/epubinfo.feature +6 -0
  22. data/features/step_definitions/epubinfo_steps.rb +5 -0
  23. data/features/support/env.rb +1 -0
  24. data/lib/epub/book/features.rb +85 -0
  25. data/lib/epub/book.rb +7 -0
  26. data/lib/epub/constants.rb +48 -0
  27. data/lib/epub/content_document/navigation.rb +104 -0
  28. data/lib/epub/content_document/xhtml.rb +41 -0
  29. data/lib/epub/content_document.rb +2 -0
  30. data/lib/epub/inspector.rb +45 -0
  31. data/lib/epub/ocf/container.rb +28 -0
  32. data/lib/epub/ocf/encryption.rb +7 -0
  33. data/lib/epub/ocf/manifest.rb +6 -0
  34. data/lib/epub/ocf/metadata.rb +6 -0
  35. data/lib/epub/ocf/rights.rb +6 -0
  36. data/lib/epub/ocf/signatures.rb +6 -0
  37. data/lib/epub/ocf.rb +8 -0
  38. data/lib/epub/parser/content_document.rb +111 -0
  39. data/lib/epub/parser/ocf.rb +73 -0
  40. data/lib/epub/parser/publication.rb +200 -0
  41. data/lib/epub/parser/utils.rb +20 -0
  42. data/lib/epub/parser/version.rb +5 -0
  43. data/lib/epub/parser.rb +103 -0
  44. data/lib/epub/publication/fixed_layout.rb +208 -0
  45. data/lib/epub/publication/package/bindings.rb +31 -0
  46. data/lib/epub/publication/package/guide.rb +51 -0
  47. data/lib/epub/publication/package/manifest.rb +180 -0
  48. data/lib/epub/publication/package/metadata.rb +170 -0
  49. data/lib/epub/publication/package/spine.rb +106 -0
  50. data/lib/epub/publication/package.rb +68 -0
  51. data/lib/epub/publication.rb +2 -0
  52. data/lib/epub.rb +14 -0
  53. data/man/epubinfo.1.ronn +19 -0
  54. data/schemas/epub-nav-30.rnc +10 -0
  55. data/schemas/epub-nav-30.sch +72 -0
  56. data/schemas/epub-xhtml-30.sch +377 -0
  57. data/schemas/ocf-container-30.rnc +16 -0
  58. data/test/fixtures/book/META-INF/container.xml +6 -0
  59. data/test/fixtures/book/OPS/%E6%97%A5%E6%9C%AC%E8%AA%9E.xhtml +10 -0
  60. data/test/fixtures/book/OPS/case-sensitive.xhtml +9 -0
  61. data/test/fixtures/book/OPS/containing space.xhtml +10 -0
  62. data/test/fixtures/book/OPS/containing%20space.xhtml +10 -0
  63. data/test/fixtures/book/OPS/nav.xhtml +28 -0
  64. 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
  65. data/test/fixtures/book/OPS//346/227/245/346/234/254/350/252/236.xhtml +10 -0
  66. data/test/fixtures/book/mimetype +1 -0
  67. data/test/helper.rb +9 -0
  68. data/test/test_content_document.rb +92 -0
  69. data/test/test_epub.rb +21 -0
  70. data/test/test_fixed_layout.rb +257 -0
  71. data/test/test_inspect.rb +121 -0
  72. data/test/test_parser.rb +60 -0
  73. data/test/test_parser_content_document.rb +36 -0
  74. data/test/test_parser_fixed_layout.rb +16 -0
  75. data/test/test_parser_ocf.rb +38 -0
  76. data/test/test_parser_publication.rb +247 -0
  77. data/test/test_publication.rb +324 -0
  78. 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
@@ -0,0 +1,5 @@
1
+ module EPUB
2
+ class Parser
3
+ VERSION = "0.1.6a"
4
+ end
5
+ end
@@ -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