loading
Generated 2023-12-28T16:41:19+00:00

All Files ( 50.45% covered at 0.77 hits/line )

39 files in total.
1229 relevant lines, 620 lines covered and 609 lines missed. ( 50.45% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
lib/epub.rb 100.00 % 5 5 5 0 1.00
lib/epub/book.rb 100.00 % 7 4 4 0 1.00
lib/epub/book/features.rb 54.35 % 103 46 25 21 0.54
lib/epub/constants.rb 100.00 % 57 15 15 0 1.00
lib/epub/content_document.rb 100.00 % 2 2 2 0 1.00
lib/epub/content_document/navigation.rb 59.72 % 142 72 43 29 0.63
lib/epub/content_document/typable.rb 77.78 % 17 9 7 2 0.78
lib/epub/content_document/xhtml.rb 47.83 % 48 23 11 12 0.48
lib/epub/inspector.rb 52.00 % 50 25 13 12 0.68
lib/epub/metadata.rb 59.22 % 197 103 61 42 1.25
lib/epub/ocf.rb 100.00 % 8 5 5 0 2.20
lib/epub/ocf/container.rb 76.92 % 29 13 10 3 0.77
lib/epub/ocf/encryption.rb 100.00 % 7 4 4 0 1.00
lib/epub/ocf/manifest.rb 100.00 % 6 3 3 0 1.00
lib/epub/ocf/metadata.rb 100.00 % 7 4 4 0 1.00
lib/epub/ocf/physical_container.rb 71.05 % 65 38 27 11 1.11
lib/epub/ocf/physical_container/archive_zip.rb 25.81 % 55 31 8 23 0.26
lib/epub/ocf/physical_container/rubyzip.rb 94.74 % 38 19 18 1 3.16
lib/epub/ocf/physical_container/unpacked_directory.rb 66.67 % 17 9 6 3 0.67
lib/epub/ocf/physical_container/unpacked_uri.rb 66.67 % 28 12 8 4 0.67
lib/epub/ocf/rights.rb 100.00 % 6 3 3 0 1.00
lib/epub/ocf/signatures.rb 100.00 % 6 3 3 0 1.00
lib/epub/parser.rb 34.78 % 109 46 16 30 0.35
lib/epub/parser/content_document.rb 24.14 % 111 58 14 44 0.24
lib/epub/parser/metadata.rb 10.77 % 104 65 7 58 0.11
lib/epub/parser/ocf.rb 44.23 % 89 52 23 29 0.44
lib/epub/parser/publication.rb 21.51 % 141 93 20 73 0.22
lib/epub/parser/xml_document.rb 69.23 % 28 13 9 4 1.00
lib/epub/parser/xml_document/refinements/nokogiri.rb 76.47 % 33 17 13 4 0.76
lib/epub/parser/xml_document/refinements/oga.rb 64.86 % 77 37 24 13 0.95
lib/epub/parser/xml_document/refinements/rexml.rb 72.00 % 50 25 18 7 1.00
lib/epub/publication.rb 100.00 % 2 2 2 0 1.00
lib/epub/publication/fixed_layout.rb 56.03 % 208 116 65 51 1.64
lib/epub/publication/package.rb 62.16 % 81 37 23 14 0.95
lib/epub/publication/package/bindings.rb 70.59 % 31 17 12 5 0.71
lib/epub/publication/package/guide.rb 53.13 % 54 32 17 15 1.53
lib/epub/publication/package/manifest.rb 37.50 % 252 112 42 70 0.38
lib/epub/publication/package/metadata.rb 100.00 % 9 5 5 0 1.00
lib/epub/publication/package/spine.rb 46.30 % 106 54 25 29 0.46

lib/epub.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 require 'epub/inspector'
  2. 1 require 'epub/ocf'
  3. 1 require 'epub/publication'
  4. 1 require 'epub/content_document'
  5. 1 require 'epub/book/features'

lib/epub/book.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 require 'epub'
  2. 1 module EPUB
  3. 1 class Book
  4. 1 include EPUB::Book::Features
  5. end
  6. end

lib/epub/book/features.rb

54.35% lines covered

46 relevant lines. 25 lines covered and 21 lines missed.
    
  1. 1 require 'forwardable'
  2. 1 module EPUB
  3. 1 class Book
  4. 1 module Features
  5. 1 extend Forwardable
  6. 1 attr_reader :ocf
  7. 1 attr_accessor :epub_file
  8. # When writing, sets +ocf.book+ to self.
  9. # @param [OCF]
  10. 1 def ocf=(mod)
  11. @ocf = mod
  12. mod.book = self
  13. mod
  14. end
  15. # @return [Array<OCF::Container::Rootfile>]
  16. 1 def rootfiles
  17. ocf.container.rootfiles
  18. end
  19. # @return [Array<Publication::Package>]
  20. 1 def packages
  21. rootfiles.map(&:package)
  22. end
  23. 1 alias renditions packages
  24. # First +package+ in +packages+
  25. # @return [Package|nil]
  26. 1 def default_rendition
  27. packages.first
  28. end
  29. 1 alias package default_rendition
  30. # @!parse def_delegators :package, :metadata, :manifest, :spine, :guide, :bindings
  31. 1 def_delegators :package, *Publication::Package::CONTENT_MODELS
  32. 1 def_delegators :metadata, :title, :main_title, :subtitle, :short_title, :collection_title, :edition_title, :extended_title, :description, :date, :unique_identifier, :modified, :release_identifier, :package_identifier
  33. 1 def_delegators :manifest, :nav
  34. 1 def container_adapter
  35. @adapter || OCF::PhysicalContainer.adapter
  36. end
  37. 1 def container_adapter=(adapter)
  38. @adapter = OCF::PhysicalContainer.find_adapter(adapter)
  39. end
  40. # Cover image defined in EPUB 3 or used in EPUB 2
  41. # @return [EPUB::Publication::Package::Manifest::Item]
  42. 1 def cover_image
  43. manifest.cover_image || metadata.cover_image
  44. end
  45. # @overload each_page_on_spine(&blk)
  46. # iterate over items in order of spine when block given
  47. # @yieldparam item [Publication::Package::Manifest::Item]
  48. # @overload each_page_on_spine
  49. # @return [Enumerator] which iterates over {Publication::Package::Manifest::Item}s in order of spine when block not given
  50. 1 def each_page_on_spine(&blk)
  51. enum = package.spine.items
  52. if block_given?
  53. enum.each &blk
  54. else
  55. enum.each
  56. end
  57. end
  58. 1 def each_page_on_toc(&blk)
  59. raise NotImplementedError
  60. end
  61. # @overload each_content(&blk)
  62. # iterate all items over when block given
  63. # @yieldparam item [Publication::Package::Manifest::Item]
  64. # @overload each_content
  65. # @return [Enumerator] which iterates over all {Publication::Package::Manifest::Item}s in EPUB package when block not given
  66. 1 def each_content(&blk)
  67. enum = manifest.items
  68. if block_given?
  69. enum.each &blk
  70. else
  71. enum.to_enum
  72. end
  73. end
  74. 1 def other_navigation
  75. raise NotImplementedError
  76. end
  77. # @return [Array<Publication::Package::Manifest::Item>] All {Publication::Package::Manifest::Item}s in EPUB package
  78. 1 def resources
  79. manifest.items
  80. end
  81. # Syntax sugar
  82. # @return String
  83. 1 def rootfile_path
  84. ocf.container.rootfile.full_path.to_s
  85. end
  86. end
  87. end
  88. end

lib/epub/constants.rb

100.0% lines covered

15 relevant lines. 15 lines covered and 0 lines missed.
    
  1. 1 module EPUB
  2. NAMESPACES = {
  3. 1 'xml' => 'http://www.w3.org/XML/1998/namespace',
  4. 'dc' => 'http://purl.org/dc/elements/1.1/',
  5. 'ocf' => 'urn:oasis:names:tc:opendocument:xmlns:container',
  6. 'opf' => 'http://www.idpf.org/2007/opf',
  7. 'xhtml' => 'http://www.w3.org/1999/xhtml',
  8. 'epub' => 'http://www.idpf.org/2007/ops',
  9. 'm' => 'http://www.w3.org/1998/Math/MathML',
  10. 'svg' => 'http://www.w3.org/2000/svg',
  11. 'smil' => 'http://www.w3.org/ns/SMIL',
  12. 'metadata' => 'http://www.idpf.org/2013/metadata'
  13. }
  14. 1 module MediaType
  15. 1 class UnsupportedMediaType < StandardError; end
  16. 1 EPUB = 'application/epub+zip'
  17. 1 ROOTFILE = 'application/oebps-package+xml'
  18. IMAGE = %w[
  19. 1 image/gif
  20. image/jpeg
  21. image/png
  22. image/svg+xml
  23. ]
  24. APPLICATION = %w[
  25. 1 application/xhtml+xml
  26. application/x-dtbncx+xml
  27. application/vnd.ms-opentype
  28. application/font-woff
  29. application/smil+xml
  30. application/pls+xml
  31. ]
  32. AUDIO = %w[
  33. 1 audio/mpeg
  34. audio/mp4
  35. ]
  36. TEXT = %w[
  37. 1 text/css
  38. text/javascript
  39. ]
  40. 1 CORE = IMAGE + APPLICATION + AUDIO + TEXT
  41. end
  42. # @see https://idpf.github.io/epub-vocabs/structure/
  43. 1 module Type
  44. DOCUMENT_NAVIGATION = %w[
  45. 1 toc
  46. landmarks
  47. ]
  48. PAGINATION = %w[
  49. 1 pagebreak
  50. page_list
  51. ]
  52. 1 TYPES = DOCUMENT_NAVIGATION + PAGINATION
  53. end
  54. end

lib/epub/content_document.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. 1 require 'epub/content_document/xhtml'
  2. 1 require 'epub/content_document/navigation'

lib/epub/content_document/navigation.rb

59.72% lines covered

72 relevant lines. 43 lines covered and 29 lines missed.
    
  1. 1 require "epub/content_document/typable"
  2. 1 module EPUB
  3. 1 module ContentDocument
  4. 1 class Navigation < XHTML
  5. 1 include Typable
  6. 1 attr_accessor :navigations
  7. 1 def initialize
  8. @navigations = []
  9. @hidden = false
  10. @parent = nil
  11. super
  12. end
  13. 1 def toc
  14. navigations.find(&:toc?)
  15. end
  16. 1 def page_list
  17. navigations.find(&:page_list?)
  18. end
  19. 1 def landmarks
  20. navigations.find(&:landmarks?)
  21. end
  22. # Enumerator version of toc
  23. 1 def contents
  24. enum_for(:each_content).to_a
  25. end
  26. # Enumerator version of page_list
  27. # Usage: navigation.enum_for(:pages)
  28. 1 def pages
  29. raise NotImplementedError
  30. end
  31. # @todo Enumerator version of landmarks
  32. # iterator for #toc
  33. 1 def each_content
  34. toc.traverse do |content, _|
  35. yield content
  36. end
  37. end
  38. # iterator for #page_list
  39. 1 def each_page
  40. raise NotImplementedError
  41. end
  42. # iterator for #landmark
  43. 1 def each_landmark
  44. raise NotImplementedError
  45. end
  46. 1 def navigation
  47. navigations.first
  48. end
  49. 1 module Hidable
  50. 1 attr_accessor :hidden, :parent
  51. 1 def hidden?
  52. if @hidden.nil?
  53. @parent ? @parent.hidden? : false
  54. else
  55. true
  56. end
  57. end
  58. end
  59. 1 class Item
  60. 1 include Hidable
  61. 1 include Typable
  62. 1 attr_accessor :items, :text,
  63. :content_document, :item
  64. 1 attr_reader :href
  65. 1 def initialize
  66. @items = ItemList.new
  67. @items.parent = self
  68. end
  69. 1 def href=(iri)
  70. @href = iri.kind_of?(Addressable::URI) ? iri : Addressable::URI.parse(iri)
  71. end
  72. 1 def traverse(depth=0, &block)
  73. block.call self, depth
  74. items.each do |item|
  75. item.traverse depth + 1, &block
  76. end
  77. end
  78. # For backward compatibility
  79. 1 def type
  80. types.find {|t|
  81. Type::TYPES.include? t
  82. }
  83. end
  84. # For backward compatibility
  85. 1 def type=(t)
  86. types << t
  87. end
  88. 1 %w[toc page_list landmarks].each do |type|
  89. 3 define_method "#{type}?" do
  90. types.include? type
  91. end
  92. end
  93. end
  94. # @todo Implement method to represent navigation structure
  95. 1 class Navigation < Item
  96. 1 module Type
  97. 1 TOC = 'toc'
  98. 1 PAGE_LIST = 'page_list'
  99. 1 LANDMARKS = 'landmarks'
  100. end
  101. 1 alias navigations items
  102. 1 alias navigations= items=
  103. 1 alias heading text
  104. 1 alias heading= text=
  105. end
  106. 1 class ItemList < Array
  107. 1 include Hidable
  108. 1 def <<(item)
  109. super
  110. item.parent = self
  111. end
  112. end
  113. end
  114. end
  115. end

lib/epub/content_document/typable.rb

77.78% lines covered

9 relevant lines. 7 lines covered and 2 lines missed.
    
  1. 1 require "set"
  2. 1 module EPUB
  3. 1 module ContentDocument
  4. 1 module Typable
  5. 1 attr_reader :types
  6. 1 def types
  7. @types ||= Set.new
  8. end
  9. 1 def types=(ts)
  10. @types = ts.kind_of?(Set) ? ts : Set.new(ts)
  11. end
  12. end
  13. end
  14. end

lib/epub/content_document/xhtml.rb

47.83% lines covered

23 relevant lines. 11 lines covered and 12 lines missed.
    
  1. 1 module EPUB
  2. 1 module ContentDocument
  3. 1 class XHTML
  4. 1 attr_accessor :item
  5. # @param [Boolean] detect_encoding See {Publication::Package::Manifest::Item#read}
  6. # @return [String] Returns the content string.
  7. 1 def read(detect_encoding: true)
  8. item.read(detect_encoding: detect_encoding)
  9. end
  10. 1 alias raw_document read
  11. # @return [true|false] Whether referenced directly from spine or not.
  12. 1 def top_level?
  13. !! item.itemref
  14. end
  15. # @return [String] Returns the value of title element.
  16. # If none, returns empty string
  17. 1 def title
  18. title_elem = rexml.get_elements('.//title').first
  19. if title_elem
  20. title_elem.text
  21. else
  22. warn 'title element not found'
  23. ''
  24. end
  25. end
  26. # @return [REXML::Document] content as REXML::Document object
  27. 1 def rexml
  28. @rexml ||= REXML::Document.new(raw_document)
  29. end
  30. # @return [Oga::XML::Document] content as Oga::XML::Document object
  31. 1 def oga
  32. require "oga"
  33. @oga ||= Oga.parse_xml(raw_document)
  34. end
  35. # @return [Nokogiri::XML::Document] content as Nokogiri::XML::Document object
  36. 1 def nokogiri
  37. require 'nokogiri'
  38. @nokogiri ||= Nokogiri.XML(raw_document)
  39. end
  40. end
  41. end
  42. end

lib/epub/inspector.rb

52.0% lines covered

25 relevant lines. 13 lines covered and 12 lines missed.
    
  1. 1 module EPUB
  2. 1 module Inspector
  3. 1 INSTANCE_VARIABLES_OPTION = {:exclude => []}
  4. 1 SIMPLE_TEMPLATE = "#<%{class}:%{object_id}>"
  5. 1 def inspect_simply
  6. SIMPLE_TEMPLATE % {
  7. :class => self.class,
  8. :object_id => inspect_object_id
  9. }
  10. end
  11. 1 def inspect_object_id
  12. (__id__ << 1).to_s(16)
  13. end
  14. 1 def inspect_instance_variables(options={})
  15. options = INSTANCE_VARIABLES_OPTION.merge(options)
  16. exclude = options[:exclude]
  17. (instance_variables - exclude).map {|name|
  18. value = instance_variable_get(name)
  19. "#{name}=#{value.inspect}"
  20. }.join(' ')
  21. end
  22. 1 module PublicationModel
  23. 1 class << self
  24. 1 def included(mod)
  25. 5 mod.__send__ :include, Inspector
  26. end
  27. end
  28. 1 def inspect
  29. template % {
  30. :class => self.class,
  31. :package => (package && package.inspect_simply),
  32. :object_id => inspect_object_id,
  33. :attributes => inspect_instance_variables(exclude: [:@package])
  34. }
  35. end
  36. 1 def template
  37. t = "#<%{class}:%{object_id}"
  38. t << " @package=%{package}" if package
  39. t << " %{attributes}>"
  40. end
  41. end
  42. end
  43. end

lib/epub/metadata.rb

59.22% lines covered

103 relevant lines. 61 lines covered and 42 lines missed.
    
  1. 1 require 'set'
  2. 1 module EPUB
  3. 1 class Metadata
  4. 1 include Inspector::PublicationModel
  5. 1 DC_ELEMS = [:identifiers, :titles, :languages] +
  6. [:contributors, :coverages, :creators, :dates, :descriptions, :formats, :publishers,
  7. :relations, :rights, :sources, :subjects, :types]
  8. # Used for CFI
  9. 1 attr_reader :children
  10. 1 attr_accessor :package, :unique_identifier, :metas, :links,
  11. 16 *(DC_ELEMS.collect {|elem| "dc_#{elem}"})
  12. 1 DC_ELEMS.each do |elem|
  13. 15 alias_method elem, "dc_#{elem}"
  14. 15 alias_method "#{elem}=", "dc_#{elem}="
  15. end
  16. 1 def initialize
  17. (DC_ELEMS + [:metas, :links]).each do |elem|
  18. __send__ "#{elem}=", []
  19. end
  20. @children = []
  21. end
  22. 1 def release_identifier
  23. "#{unique_identifier}@#{modified}"
  24. end
  25. 1 alias package_identifier release_identifier
  26. 1 def title
  27. return extended_title unless extended_title.empty?
  28. compositted = titles.select {|title| title.display_seq}.sort.join("\n")
  29. return compositted unless compositted.empty?
  30. return main_title unless main_title.empty?
  31. titles.sort.join("\n")
  32. end
  33. 1 %w[main short collection edition extended].each do |type|
  34. 5 define_method "#{type}_title" do
  35. titles.select {|title| title.title_type.to_s == type}.sort.join(' ')
  36. end
  37. end
  38. 1 def subtitle
  39. titles.select {|title| title.title_type.to_s == 'subtitle'}.sort.join(' ')
  40. end
  41. 1 def description
  42. descriptions.join(' ')
  43. end
  44. 1 def date
  45. dates.first
  46. end
  47. 1 def language
  48. languages.first
  49. end
  50. 1 def modified
  51. metas.find {|meta|
  52. meta.property == 'dcterms:modified' &&
  53. meta.refiners.empty?
  54. }
  55. end
  56. # Cover image used in EPUB 2
  57. # @return [EPUB::Publication::Package::Manifest::Item]
  58. 1 def cover_image
  59. cover_image_meta = metas.find {|meta| meta.name == "cover"}
  60. return unless cover_image_meta
  61. package.manifest[cover_image_meta.meta_content]
  62. end
  63. 1 def to_h
  64. DC_ELEMS.inject({}) do |hsh, elem|
  65. hsh[elem] = __send__(elem)
  66. hsh
  67. end
  68. end
  69. 1 def primary_metas
  70. metas.select {|meta| meta.primary_expression?}
  71. end
  72. 1 module Refinee
  73. 1 PROPERTIES = %w[alternate-script display-seq file-as group-position identifier-type meta-auth role title-type]
  74. 1 attr_writer :refiners
  75. 1 def refiners
  76. @refiners ||= Set.new
  77. end
  78. 1 PROPERTIES.each do |voc|
  79. 8 met = voc.gsub(/-/, '_')
  80. 8 attr_writer met
  81. 8 define_method met do
  82. refiners.find {|refiner| refiner.property == voc}
  83. end
  84. end
  85. end
  86. 1 class DCMES
  87. 1 include Refinee
  88. 1 attr_accessor :content, :id, :lang, :dir
  89. 1 def to_s
  90. content.to_s
  91. end
  92. end
  93. 1 class Identifier < DCMES
  94. # @note This is ad-hoc
  95. # @todo Define and include OPF module for opf:scheme attribute
  96. # @todo Define general way to handle with identifier-type refiners
  97. 1 attr_accessor :scheme
  98. # @note This is ad-hoc
  99. # @todo Define and include OPF module for opf:scheme attribute
  100. # @todo Define general way to handle with identifier-type refiners
  101. 1 def isbn?
  102. scheme == 'ISBN' or
  103. content.to_s.downcase.start_with? 'urn:isbn' or
  104. refiners.any? {|refiner|
  105. refiner.property == 'identifier-type' and
  106. refiner.scheme == 'onix:codelist5' and
  107. %w[02 15].include? refiner.content
  108. }
  109. end
  110. end
  111. 1 class Title < DCMES
  112. 1 include Comparable
  113. 1 def <=>(other)
  114. return 1 if other.display_seq.nil?
  115. return -1 if display_seq.nil?
  116. display_seq.to_s.to_i <=> other.display_seq.to_s.to_i
  117. end
  118. end
  119. 1 class Meta
  120. 1 include Refinee
  121. 1 attr_accessor :property, :id, :scheme, :content, :name, :meta_content
  122. 1 attr_reader :refines
  123. 1 def refines=(refinee)
  124. refinee.refiners << self
  125. @refines = refinee
  126. end
  127. 1 def refines?
  128. ! refines.nil?
  129. end
  130. 1 alias subexpression? refines?
  131. 1 def primary_expression?
  132. ! subexpression?
  133. end
  134. 1 def inspect
  135. ivs = instance_variables.map {|iv|
  136. [iv, instance_variable_get(iv).inspect].join('=')
  137. }.join(' ')
  138. '<#%s:%#0x %s>' % [self.class, __id__, ivs]
  139. end
  140. 1 def to_s
  141. content.to_s
  142. end
  143. end
  144. 1 class Link
  145. 1 include Refinee
  146. 1 attr_accessor :href, :rel, :id, :media_type
  147. 1 attr_reader :refines
  148. 1 def refines=(refinee)
  149. refinee.refiners << self
  150. @refines = refinee
  151. end
  152. end
  153. 1 class UnsupportedModel
  154. 1 attr_accessor :raw_element
  155. 1 def initialize(raw_element)
  156. @raw_element = raw_element
  157. end
  158. end
  159. end
  160. end

lib/epub/ocf.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 module EPUB
  2. 1 class OCF
  3. 1 MODULES = %w[container encryption manifest metadata rights signatures]
  4. 7 MODULES.each {|m| require "epub/ocf/#{m}"}
  5. 1 attr_accessor :book, *MODULES
  6. end
  7. end

lib/epub/ocf/container.rb

76.92% lines covered

13 relevant lines. 10 lines covered and 3 lines missed.
    
  1. 1 module EPUB
  2. 1 class OCF
  3. 1 class Container
  4. 1 FILE = 'container.xml'
  5. 1 attr_reader :rootfiles
  6. 1 def initialize
  7. @rootfiles = []
  8. end
  9. # syntax sugar
  10. 1 def rootfile
  11. rootfiles.first
  12. end
  13. 1 class Rootfile
  14. 1 attr_accessor :full_path, :media_type,
  15. :package
  16. # @param full_path [Addressable::URI|nil]
  17. # @param media_type [String]
  18. 1 def initialize(full_path=nil, media_type=EPUB::MediaType::ROOTFILE)
  19. @full_path, @media_type = full_path, media_type
  20. end
  21. end
  22. end
  23. end
  24. end

lib/epub/ocf/encryption.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 module EPUB
  2. 1 class OCF
  3. 1 class Encryption
  4. 1 attr_accessor :content
  5. end
  6. end
  7. end

lib/epub/ocf/manifest.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 module EPUB
  2. 1 class OCF
  3. 1 class Manifest
  4. end
  5. end
  6. end

lib/epub/ocf/metadata.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. 1 module EPUB
  2. 1 class OCF
  3. 1 class UnknownFormatMetadata
  4. 1 attr_accessor :content
  5. end
  6. end
  7. end

lib/epub/ocf/physical_container.rb

71.05% lines covered

38 relevant lines. 27 lines covered and 11 lines missed.
    
  1. 1 require 'monitor'
  2. 1 require 'epub/ocf/physical_container/archive_zip'
  3. 1 require 'epub/ocf/physical_container/unpacked_directory'
  4. 1 require 'epub/ocf/physical_container/unpacked_uri'
  5. 1 module EPUB
  6. 1 class OCF
  7. 1 class PhysicalContainer
  8. 1 class NoEntry < StandardError
  9. 1 class << self
  10. 1 def from_error(error)
  11. no_entry = new(error.message)
  12. no_entry.set_backtrace error.backtrace
  13. no_entry
  14. end
  15. end
  16. end
  17. 1 @adapter = ArchiveZip
  18. 1 class << self
  19. 1 def find_adapter(adapter)
  20. return adapter if adapter.instance_of? Class
  21. if adapter == :Zipruby && ! const_defined?(adapter)
  22. require 'epub/ocf/physical_container/zipruby'
  23. end
  24. const_get adapter
  25. end
  26. 1 def adapter
  27. raise NoMethodError, "undefined method `#{__method__}' for #{self}" unless self == PhysicalContainer
  28. @adapter
  29. end
  30. 1 def adapter=(adapter)
  31. raise NoMethodError, "undefined method `#{__method__}' for #{self}" unless self == PhysicalContainer
  32. @adapter = find_adapter(adapter)
  33. end
  34. 1 def open(container_path)
  35. 2 _adapter.new(container_path).open do |container|
  36. 2 yield container
  37. end
  38. end
  39. 1 def read(container_path, path_name)
  40. 1 open(container_path) {|container|
  41. 1 container.read(path_name.to_s)
  42. }
  43. end
  44. 1 private
  45. 1 def _adapter
  46. 2 (self == PhysicalContainer) ? @adapter : self
  47. end
  48. end
  49. 1 def initialize(container_path)
  50. 7 @container_path = container_path
  51. 7 @monitor = Monitor.new
  52. end
  53. end
  54. end
  55. end

lib/epub/ocf/physical_container/archive_zip.rb

25.81% lines covered

31 relevant lines. 8 lines covered and 23 lines missed.
    
  1. 1 require 'archive/zip'
  2. 1 module EPUB
  3. 1 class OCF
  4. 1 class PhysicalContainer
  5. 1 class ArchiveZip < self
  6. 1 def initialize(container_path)
  7. super
  8. @entries = {}
  9. @last_iterated_entry_index = 0
  10. end
  11. 1 def open
  12. Archive::Zip.open @container_path do |archive|
  13. @monitor.synchronize do
  14. @archive = archive
  15. begin
  16. yield self
  17. ensure
  18. @archive = nil
  19. end
  20. end
  21. end
  22. end
  23. 1 def read(path_name)
  24. if @archive
  25. target_index = @entries[path_name]
  26. @archive.each.with_index do |entry, index|
  27. if target_index
  28. if target_index == index
  29. return entry.file_data.read
  30. else
  31. next
  32. end
  33. end
  34. next if index < @last_iterated_entry_index
  35. # We can force encoding UTF-8 because EPUB spec allows only UTF-8 filenames
  36. entry_path = entry.zip_path.force_encoding('UTF-8')
  37. @entries[entry_path] = index
  38. @last_iterated_entry_index = index
  39. if entry_path == path_name
  40. return entry.file_data.read
  41. end
  42. end
  43. raise NoEntry, "Entry not found: #{path_name}"
  44. else
  45. open {|container| container.read(path_name)}
  46. end
  47. end
  48. end
  49. end
  50. end
  51. end

lib/epub/ocf/physical_container/rubyzip.rb

94.74% lines covered

19 relevant lines. 18 lines covered and 1 lines missed.
    
  1. 1 require "zip"
  2. 1 module EPUB
  3. 1 class OCF
  4. 1 class PhysicalContainer
  5. 1 class Rubyzip < self
  6. 1 def open
  7. 5 orig_encoding = Zip.force_entry_names_encoding
  8. begin
  9. 5 Zip.force_entry_names_encoding = "UTF-8"
  10. 5 Zip::File.open @container_path do |archive|
  11. 5 @monitor.synchronize do
  12. 5 @archive = archive
  13. begin
  14. 5 yield self
  15. ensure
  16. 5 @archive = nil
  17. end
  18. end
  19. end
  20. ensure
  21. 5 Zip.force_entry_names_encoding = orig_encoding
  22. end
  23. end
  24. 1 def read(path_name)
  25. 6 if @archive
  26. 5 @archive.read(path_name)
  27. else
  28. 2 open {|container| container.read(path_name)}
  29. end
  30. rescue Errno::ENOENT => error
  31. raise NoEntry.from_error(error)
  32. end
  33. end
  34. end
  35. end
  36. end

lib/epub/ocf/physical_container/unpacked_directory.rb

66.67% lines covered

9 relevant lines. 6 lines covered and 3 lines missed.
    
  1. 1 module EPUB
  2. 1 class OCF
  3. 1 class PhysicalContainer
  4. 1 class UnpackedDirectory < self
  5. 1 def open
  6. yield self
  7. end
  8. 1 def read(path_name)
  9. ::File.read(::File.join(@container_path, path_name))
  10. rescue ::Errno::ENOENT => error
  11. raise NoEntry.from_error(error)
  12. end
  13. end
  14. end
  15. end
  16. end

lib/epub/ocf/physical_container/unpacked_uri.rb

66.67% lines covered

12 relevant lines. 8 lines covered and 4 lines missed.
    
  1. 1 require 'open-uri'
  2. 1 module EPUB
  3. 1 class OCF
  4. 1 class PhysicalContainer
  5. 1 class UnpackedURI < self
  6. # EPUB URI: http://example.net/path/to/book/
  7. # container.xml: http://example.net/path/to/book/META-INF/container.xml
  8. # @param [URI, String] container_path URI of EPUB container's root directory.
  9. # For exapmle, <code>"http://example.net/path/to/book/"</code>, which
  10. # should contain <code>"http://example.net/path/to/book/META-INF/container.xml"</code> as its container.xml file. Note that this should end with "/"(slash).
  11. 1 def initialize(container_path)
  12. super(URI(container_path))
  13. end
  14. 1 def open
  15. yield self
  16. end
  17. 1 def read(path_name)
  18. (@container_path + path_name).read
  19. rescue ::OpenURI::HTTPError => error
  20. raise NoEntry.from_error(error)
  21. end
  22. end
  23. end
  24. end
  25. end

lib/epub/ocf/rights.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 module EPUB
  2. 1 class OCF
  3. 1 class Rights
  4. end
  5. end
  6. end

lib/epub/ocf/signatures.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. 1 module EPUB
  2. 1 class OCF
  3. 1 class Signatures
  4. end
  5. end
  6. end

lib/epub/parser.rb

34.78% lines covered

46 relevant lines. 16 lines covered and 30 lines missed.
    
  1. 1 require 'epub'
  2. 1 require 'epub/constants'
  3. 1 require 'epub/book'
  4. 1 module EPUB
  5. 1 class Parser
  6. 1 class << self
  7. # Parse an EPUB file
  8. #
  9. # @example
  10. # EPUB::Parser.parse('path/to/book.epub') # => EPUB::Book object
  11. #
  12. # @example
  13. # class MyBook
  14. # include EPUB::Book::Feature
  15. # end
  16. # book = MyBook.new
  17. # parsed_book = EPUB::Parser.parse('path/to/book.epub', book: book) # => #<MyBook:0x000000019760e8 @epub_file=..>
  18. # parsed_book.equal? book # => true
  19. #
  20. # @example
  21. # book = EPUB::Parser.parse('path/to/book.epub', class: MyBook) # => #<MyBook:0x000000019b0568 @epub_file=...>
  22. # book.instance_of? MyBook # => true
  23. #
  24. # @param [String] filepath
  25. # @param [Hash] options the type of return is specified by this argument.
  26. # If no options, returns {EPUB::Book} object.
  27. # For details of options, see below.
  28. # @option options [EPUB] :book instance of class which includes {EPUB} module
  29. # @option options [Class] :class class which includes {EPUB} module
  30. # @option options [EPUB::OCF::PhysicalContainer, Symbol] :container_adapter OCF physical container adapter to use when parsing EPUB container
  31. # When class passed, it is used. When symbol passed, it is considered as subclass name of {EPUB::OCF::PhysicalContainer}.
  32. # If omitted, {EPUB::OCF::PhysicalContainer.adapter} is used.
  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. 1 def parse(filepath, container_adapter: nil, book: nil, initialize_with: nil, **options)
  38. new(filepath, container_adapter: container_adapter, book: book, initialize_with: initialize_with, **options).parse
  39. end
  40. end
  41. 1 def initialize(filepath, container_adapter: nil, book: nil, initialize_with: nil, **options)
  42. if filepath.to_s.encoding == Encoding::ASCII_8BIT
  43. # On Windows and macOS, encoding of file name is set by Ruby,
  44. # but on UNIX, always is ASCII-8BIT
  45. # See https://docs.ruby-lang.org/ja/2.7.0/class/IO.html
  46. filepath = filepath.to_s.dup
  47. require "nkf"
  48. filepath.force_encoding NKF.guess(filepath)
  49. end
  50. path_is_uri = (container_adapter == EPUB::OCF::PhysicalContainer::UnpackedURI or
  51. container_adapter == :UnpackedURI or
  52. EPUB::OCF::PhysicalContainer.adapter == EPUB::OCF::PhysicalContainer::UnpackedURI)
  53. raise "File #{filepath} not found" if
  54. !path_is_uri and !File.exist?(filepath)
  55. @filepath = path_is_uri ? filepath : File.realpath(filepath)
  56. @book = create_book(book: book, initialize_with: initialize_with, **options)
  57. if path_is_uri
  58. @book.container_adapter = :UnpackedURI
  59. elsif File.directory? @filepath
  60. @book.container_adapter = :UnpackedDirectory
  61. end
  62. @book.epub_file = @filepath
  63. if options[:container_adapter]
  64. @book.container_adapter = options[:container_adapter]
  65. end
  66. end
  67. 1 def parse
  68. @book.container_adapter.open @filepath do |container|
  69. @book.ocf = OCF.parse(container)
  70. @book.ocf.container.rootfiles.each {|rootfile|
  71. package = Publication.parse(container, rootfile.full_path.to_s)
  72. rootfile.package = package
  73. @book.packages << package
  74. package.book = @book
  75. }
  76. end
  77. @book
  78. end
  79. 1 private
  80. 1 def create_book(book: nil, initialize_with: nil, **params)
  81. case
  82. when book
  83. book
  84. when params[:class]
  85. if initialize_with
  86. params[:class].new initialize_with
  87. else
  88. params[:class].new
  89. end
  90. else
  91. Book.new
  92. end
  93. end
  94. end
  95. end
  96. 1 require 'epub/parser/version'
  97. 1 require 'epub/parser/xml_document'
  98. 1 require 'epub/parser/ocf'
  99. 1 require 'epub/parser/publication'
  100. 1 require 'epub/parser/content_document'

lib/epub/parser/content_document.rb

24.14% lines covered

58 relevant lines. 14 lines covered and 44 lines missed.
    
  1. 1 require 'epub/content_document'
  2. 1 require 'epub/constants'
  3. 1 require 'epub/parser/xml_document'
  4. 1 module EPUB
  5. 1 class Parser
  6. 1 class ContentDocument
  7. 1 using XMLDocument::Refinements
  8. # @param [EPUB::Publication::Package::Manifest::Item] item
  9. 1 def initialize(item)
  10. @item = item
  11. end
  12. 1 def parse
  13. content_document = case @item.media_type
  14. when 'application/xhtml+xml'
  15. if @item.nav?
  16. EPUB::ContentDocument::Navigation.new
  17. else
  18. EPUB::ContentDocument::XHTML.new
  19. end
  20. when 'image/svg+xml'
  21. EPUB::ContentDocument::SVG.new
  22. else
  23. nil
  24. end
  25. return content_document if content_document.nil?
  26. content_document.item = @item
  27. document = XMLDocument.new(@item.read)
  28. # parse_content_document(document)
  29. if @item.nav?
  30. content_document.navigations = parse_navigations(document)
  31. end
  32. content_document
  33. end
  34. # @param [XMLDocument, REXML::Document, Oga::XML::Document, Nokogiri::HTML::Document] document HTML document or element including nav
  35. # @return [Array<EPUB::ContentDocument::Navigation::Navigation>] navs array of Navigation object
  36. 1 def parse_navigations(document)
  37. document.each_element_by_xpath('/xhtml:html/xhtml:body//xhtml:nav', EPUB::NAMESPACES).collect {|elem| parse_navigation elem}
  38. end
  39. # @param [REXML::Element, Oga::XML::Element, Nokogiri::XML::Element] element nav element
  40. # @return [EPUB::ContentDocument::Navigation::Navigation] nav Navigation object
  41. 1 def parse_navigation(element)
  42. nav = EPUB::ContentDocument::Navigation::Navigation.new
  43. nav.text = find_heading(element)
  44. hidden = element.attribute_with_prefix('hidden')
  45. nav.hidden = hidden.nil? ? nil : true
  46. nav.types = element.attribute_with_prefix('type', 'epub')&.split(/\s+/)
  47. element.each_element_by_xpath('./xhtml:ol/xhtml:li', EPUB::NAMESPACES).map do |elem|
  48. nav.items << parse_navigation_item(elem)
  49. end
  50. nav
  51. end
  52. # @param [REXML::Element, Oga::XML::Element, Nokogiri::XML::Element] element li element
  53. 1 def parse_navigation_item(element)
  54. item = EPUB::ContentDocument::Navigation::Item.new
  55. a_or_span = element.each_element_by_xpath('./xhtml:a[1]|xhtml:span[1]', EPUB::NAMESPACES).first
  56. return a_or_span if a_or_span.nil?
  57. item.text = a_or_span.content
  58. item.types = a_or_span.attribute_with_prefix('type', 'epub')&.split(/\s+/)
  59. if a_or_span.name == 'a'
  60. if item.text.empty?
  61. embedded_content = a_or_span.each_element_by_xpath('./xhtml:audio[1]|xhtml:canvas[1]|xhtml:embed[1]|xhtml:iframe[1]|xhtml:img[1]|xhtml:math[1]|xhtml:object[1]|xhtml:svg[1]|xhtml:video[1]', EPUB::NAMESPACES).first
  62. unless embedded_content.nil?
  63. case embedded_content.name
  64. when 'audio', 'canvas', 'embed', 'iframe'
  65. item.text = embedded_content.attribute_with_prefix('name') || embedded_content.attribute_with_prefix('srcdoc')
  66. when 'img'
  67. item.text = embedded_content.attribute_with_prefix('alt')
  68. when 'math', 'object'
  69. item.text = embedded_content.attribute_with_prefix('name')
  70. when 'svg', 'video'
  71. else
  72. end
  73. end
  74. item.text = a_or_span.attribute_with_prefix('title').to_s if item.text.nil? || item.text.empty?
  75. end
  76. item.href = a_or_span.attribute_with_prefix('href')
  77. item.item = @item.find_item_by_relative_iri(item.href)
  78. end
  79. item.items = element.each_element_by_xpath('./xhtml:ol[1]/xhtml:li', EPUB::NAMESPACES).map {|li| parse_navigation_item(li)}
  80. item
  81. end
  82. 1 private
  83. # @param [REXML::Element, Oga::XML::Element, Nokogiri::XML::Element] element nav element
  84. # @return [String] heading heading text
  85. 1 def find_heading(element)
  86. heading = element.each_element_by_xpath('./xhtml:h1|xhtml:h2|xhtml:h3|xhtml:h4|xhtml:h5|xhtml:h6|xhtml:hgroup', EPUB::NAMESPACES).first
  87. return nil if heading.nil?
  88. return heading.content unless heading.name == 'hgroup'
  89. (heading.each_element_by_xpath(".//xhtml:h1", EPUB::NAMESPACES) ||
  90. heading.each_element_by_xpath(".//xhtml:h2", EPUB::NAMESPACES) ||
  91. heading.each_element_by_xpath(".//xhtml:h3", EPUB::NAMESPACES) ||
  92. heading.each_element_by_xpath(".//xhtml:h4", EPUB::NAMESPACES) ||
  93. heading.each_element_by_xpath(".//xhtml:h5", EPUB::NAMESPACES) ||
  94. heading.each_element_by_xpath(".//xhtml:h6", EPUB::NAMESPACES)).first.content
  95. end
  96. end
  97. end
  98. end

lib/epub/parser/metadata.rb

10.77% lines covered

65 relevant lines. 7 lines covered and 58 lines missed.
    
  1. 1 module EPUB
  2. 1 class Parser
  3. 1 module Metadata
  4. 1 using XMLDocument::Refinements
  5. 1 def parse_metadata(elem, unique_identifier_id, default_namespace)
  6. metadata = EPUB::Publication::Package::Metadata.new
  7. id_map = {}
  8. default_namespace_uri = EPUB::NAMESPACES[default_namespace]
  9. elem.each_element do |child|
  10. elem_name = child.name
  11. model =
  12. case child.namespace_uri
  13. when EPUB::NAMESPACES['dc']
  14. case elem_name
  15. when 'identifier'
  16. identifier = build_model(child, :Identifier, ['id'])
  17. metadata.identifiers << identifier
  18. identifier.scheme = child.attribute_with_prefix('scheme', 'opf')
  19. identifier
  20. when 'title'
  21. title = build_model(child, :Title)
  22. metadata.titles << title
  23. title
  24. when 'language'
  25. language = build_model(child, :DCMES, ['id'])
  26. metadata.languages << language
  27. language
  28. when 'contributor', 'coverage', 'creator', 'date', 'description', 'format', 'publisher', 'relation', 'source', 'subject', 'rights', 'type'
  29. attr = elem_name == 'rights' ? elem_name : elem_name + 's'
  30. dcmes = build_model(child)
  31. metadata.__send__(attr) << dcmes
  32. dcmes
  33. else
  34. build_unsupported_model(child)
  35. end
  36. when default_namespace_uri
  37. case elem_name
  38. when 'meta'
  39. meta = build_model(child, :Meta, %w[property id scheme content name])
  40. metadata.metas << meta
  41. meta
  42. when 'link'
  43. link = build_model(child, :Link, %w[id media-type])
  44. metadata.links << link
  45. link.href = child.attribute_with_prefix('href')
  46. link.rel = Set.new(child.attribute_with_prefix('rel').split(/\s+/))
  47. link
  48. else
  49. build_unsupported_model(child)
  50. end
  51. else
  52. build_unsupported_model(child)
  53. end
  54. metadata.children << model
  55. if model.kind_of?(EPUB::Metadata::Identifier) &&
  56. model.id == unique_identifier_id
  57. metadata.unique_identifier = model
  58. end
  59. if model.respond_to?(:id) && model.id
  60. id_map[model.id] = {refinee: model}
  61. end
  62. refines = child.attribute_with_prefix('refines')
  63. if refines && refines.start_with?('#')
  64. id = refines[1..-1]
  65. id_map[id] ||= {}
  66. id_map[id][:refiners] ||= []
  67. id_map[id][:refiners] << model
  68. end
  69. end
  70. id_map.values.each do |hsh|
  71. next unless hsh[:refiners]
  72. next unless hsh[:refinee]
  73. hsh[:refiners].each {|meta| meta.refines = hsh[:refinee]}
  74. end
  75. metadata
  76. end
  77. 1 def build_model(elem, klass=:DCMES, attributes=%w[id lang dir])
  78. model = EPUB::Metadata.const_get(klass).new
  79. attributes.each do |attr|
  80. writer_name = (attr == "content") ? "meta_content=" : "#{attr.gsub('-', '_')}="
  81. namespace = (attr == "lang") ? "xml" : nil
  82. model.__send__ writer_name, elem.attribute_with_prefix(attr, namespace)
  83. end
  84. model.content = elem.content unless klass == :Link
  85. model.content.strip! if klass == :Identifier
  86. model
  87. end
  88. 1 def build_unsupported_model(elem)
  89. EPUB::Metadata::UnsupportedModel.new(elem)
  90. end
  91. end
  92. end
  93. end

lib/epub/parser/ocf.rb

44.23% lines covered

52 relevant lines. 23 lines covered and 29 lines missed.
    
  1. 1 require 'epub/constants'
  2. 1 require 'epub/ocf'
  3. 1 require 'epub/ocf/physical_container'
  4. 1 require 'epub/parser/metadata'
  5. 1 require "epub/parser/xml_document"
  6. 1 module EPUB
  7. 1 class Parser
  8. 1 class OCF
  9. 1 using XMLDocument::Refinements
  10. 1 include Metadata
  11. 1 DIRECTORY = 'META-INF'
  12. 1 class << self
  13. 1 def parse(container)
  14. new(container).parse
  15. end
  16. end
  17. 1 def initialize(container)
  18. @container = container
  19. @ocf = EPUB::OCF.new
  20. end
  21. 1 def parse
  22. EPUB::OCF::MODULES.each do |m|
  23. begin
  24. data = @container.read(File.join(DIRECTORY, "#{m}.xml"))
  25. @ocf.__send__ "#{m}=", __send__("parse_#{m}", data)
  26. rescue EPUB::OCF::PhysicalContainer::NoEntry
  27. end
  28. end
  29. @ocf
  30. end
  31. 1 def parse_container(xml)
  32. container = EPUB::OCF::Container.new
  33. doc = XMLDocument.new(xml)
  34. doc.each_element_by_xpath "/ocf:container/ocf:rootfiles/ocf:rootfile", EPUB::NAMESPACES do |elem|
  35. rootfile = EPUB::OCF::Container::Rootfile.new
  36. rootfile.full_path = Addressable::URI.parse(elem.attribute_with_prefix('full-path'))
  37. rootfile.media_type = elem.attribute_with_prefix('media-type')
  38. container.rootfiles << rootfile
  39. end
  40. container
  41. end
  42. 1 def parse_encryption(content)
  43. encryption = EPUB::OCF::Encryption.new
  44. encryption.content = content
  45. encryption
  46. end
  47. 1 def parse_manifest(content)
  48. warn "Not implemented: #{self.class}##{__method__}" if $VERBOSE
  49. end
  50. 1 def parse_metadata(content)
  51. doc = XMLDocument.new(content)
  52. unless multiple_rendition_metadata?(doc)
  53. warn "Not implemented: #{self.class}##{__method__}" if $VERBOSE
  54. metadata = EPUB::OCF::UnknownFormatMetadata.new
  55. metadata.content = content
  56. return metadata
  57. end
  58. super(doc.root, doc.root.attribute_with_prefix('unique-identifier'), 'metadata')
  59. end
  60. 1 def parse_rights(content)
  61. warn "Not implemented: #{self.class}##{__method__}" if $VERBOSE
  62. end
  63. 1 def parse_signatures(content)
  64. warn "Not implemented: #{self.class}##{__method__}" if $VERBOSE
  65. end
  66. 1 private
  67. 1 def multiple_rendition_metadata?(doc)
  68. doc.root &&
  69. doc.root.name == 'metadata' &&
  70. doc.root.namespaces['xmlns'] == EPUB::NAMESPACES['metadata']
  71. end
  72. end
  73. end
  74. end

lib/epub/parser/publication.rb

21.51% lines covered

93 relevant lines. 20 lines covered and 73 lines missed.
    
  1. 1 require 'strscan'
  2. 1 require 'epub/publication'
  3. 1 require 'epub/constants'
  4. 1 require 'epub/parser/metadata'
  5. 1 module EPUB
  6. 1 class Parser
  7. 1 class Publication
  8. 1 using XMLDocument::Refinements
  9. 1 include Metadata
  10. 1 class << self
  11. 1 def parse(container, file)
  12. opf = container.read(Addressable::URI.unencode(file))
  13. new(opf).parse
  14. end
  15. end
  16. 1 def initialize(opf)
  17. @doc = XMLDocument.new(opf)
  18. end
  19. 1 def parse
  20. package = parse_package(@doc)
  21. (EPUB::Publication::Package::CONTENT_MODELS - [:bindings]).each do |model|
  22. package.__send__ "#{model}=", __send__("parse_#{model}", @doc)
  23. end
  24. package.bindings = parse_bindings(@doc, package.manifest)
  25. package
  26. end
  27. 1 def parse_package(doc)
  28. package = EPUB::Publication::Package.new
  29. elem = doc.root
  30. %w[version xml:lang dir id].each do |attr|
  31. package.__send__ "#{attr.gsub(/\:/, '_')}=", elem.attribute_with_prefix(attr)
  32. end
  33. package.prefix = parse_prefix(elem.attribute_with_prefix('prefix'))
  34. EPUB::Publication.__send__ :include, EPUB::Publication::FixedLayout if package.prefix.key? EPUB::Publication::FixedLayout::PREFIX_KEY
  35. package
  36. end
  37. 1 def parse_metadata(doc)
  38. super(doc.each_element_by_xpath('/opf:package/opf:metadata', EPUB::NAMESPACES).first, doc.root.attribute_with_prefix('unique-identifier'), 'opf')
  39. end
  40. 1 def parse_manifest(doc)
  41. manifest = EPUB::Publication::Package::Manifest.new
  42. elem = doc.each_element_by_xpath('/opf:package/opf:manifest', EPUB::NAMESPACES).first
  43. manifest.id = elem.attribute_with_prefix('id')
  44. fallback_map = {}
  45. elem.each_element_by_xpath('./opf:item', EPUB::NAMESPACES).each do |e|
  46. item = EPUB::Publication::Package::Manifest::Item.new
  47. %w[id media-type media-overlay].each do |attr|
  48. item.__send__ "#{attr.gsub(/-/, '_')}=", e.attribute_with_prefix(attr)
  49. end
  50. item.href = e.attribute_with_prefix('href')
  51. fallback = e.attribute_with_prefix('fallback')
  52. fallback_map[fallback] = item if fallback
  53. properties = e.attribute_with_prefix('properties')
  54. item.properties = properties.split(' ') if properties
  55. manifest << item
  56. end
  57. fallback_map.each_pair do |id, from|
  58. from.fallback = manifest[id]
  59. end
  60. manifest
  61. end
  62. 1 def parse_spine(doc)
  63. spine = EPUB::Publication::Package::Spine.new
  64. elem = doc.each_element_by_xpath('/opf:package/opf:spine', EPUB::NAMESPACES).first
  65. %w[id toc page-progression-direction].each do |attr|
  66. spine.__send__ "#{attr.gsub(/-/, '_')}=", elem.attribute_with_prefix(attr)
  67. end
  68. elem.each_element_by_xpath('./opf:itemref', EPUB::NAMESPACES).each do |e|
  69. itemref = EPUB::Publication::Package::Spine::Itemref.new
  70. %w[idref id].each do |attr|
  71. itemref.__send__ "#{attr}=", e.attribute_with_prefix(attr)
  72. end
  73. itemref.linear = (e.attribute_with_prefix('linear') != 'no')
  74. properties = e.attribute_with_prefix('properties')
  75. itemref.properties = properties.split(' ') if properties
  76. spine << itemref
  77. end
  78. spine
  79. end
  80. 1 def parse_guide(doc)
  81. guide = EPUB::Publication::Package::Guide.new
  82. doc.each_element_by_xpath '/opf:package/opf:guide/opf:reference', EPUB::NAMESPACES do |ref|
  83. reference = EPUB::Publication::Package::Guide::Reference.new
  84. %w[type title].each do |attr|
  85. reference.__send__ "#{attr}=", ref.attribute_with_prefix(attr)
  86. end
  87. reference.href = ref.attribute_with_prefix('href')
  88. guide << reference
  89. end
  90. guide
  91. end
  92. 1 def parse_bindings(doc, handler_map)
  93. bindings = EPUB::Publication::Package::Bindings.new
  94. doc.each_element_by_xpath '/opf:package/opf:bindings/opf:mediaType', EPUB::NAMESPACES do |elem|
  95. media_type = EPUB::Publication::Package::Bindings::MediaType.new
  96. media_type.media_type = elem.attribute_with_prefix('media-type')
  97. media_type.handler = handler_map[elem.attribute_with_prefix('handler')]
  98. bindings << media_type
  99. end
  100. bindings
  101. end
  102. 1 def parse_prefix(str)
  103. prefixes = {}
  104. return prefixes if str.nil? or str.empty?
  105. scanner = StringScanner.new(str)
  106. scanner.scan /\s*/
  107. while prefix = scanner.scan(/[^\:\s]+/)
  108. scanner.scan /[\:\s]+/
  109. iri = scanner.scan(/[^\s]+/)
  110. if iri.nil? or iri.empty?
  111. warn "no IRI detected for prefix `#{prefix}`"
  112. else
  113. prefixes[prefix] = iri
  114. end
  115. scanner.scan /\s*/
  116. end
  117. prefixes
  118. end
  119. end
  120. end
  121. end

lib/epub/parser/xml_document.rb

69.23% lines covered

13 relevant lines. 9 lines covered and 4 lines missed.
    
  1. 1 module EPUB
  2. 1 class Parser
  3. 1 class XMLDocument
  4. 1 class << self
  5. 1 attr_accessor :backend
  6. 1 def new(xml)
  7. case backend
  8. when :Oga
  9. Oga.parse_xml(xml)
  10. when :Nokogiri
  11. Nokogiri.XML(xml)
  12. else
  13. REXML::Document.new(xml)
  14. end
  15. end
  16. end
  17. end
  18. end
  19. end
  20. 1 %i[Nokogiri Oga REXML].each do |backend|
  21. begin
  22. 3 require "epub/parser/xml_document/refinements/#{backend.downcase}"
  23. 3 EPUB::Parser::XMLDocument.backend ||= backend
  24. rescue LoadError
  25. end
  26. end

lib/epub/parser/xml_document/refinements/nokogiri.rb

76.47% lines covered

17 relevant lines. 13 lines covered and 4 lines missed.
    
  1. 1 require "nokogiri"
  2. 1 module EPUB
  3. 1 class Parser
  4. 1 class XMLDocument
  5. 1 module Refinements
  6. 1 module Nokogiri
  7. 1 refine ::Nokogiri::XML::Node do
  8. 1 def each_element_by_xpath(xpath, namespaces = nil, &block)
  9. xpath(xpath, namespaces).each &block
  10. end
  11. 1 def attribute_with_prefix(name, prefix = nil)
  12. attribute_with_ns(name, EPUB::NAMESPACES[prefix])&.value
  13. end
  14. 1 def each_element(xpath = nil, &block)
  15. element_children.each(&block)
  16. end
  17. 1 alias elements element_children
  18. 1 def namespace_uri
  19. namespace.href
  20. end
  21. end
  22. end
  23. 1 include Nokogiri
  24. end
  25. end
  26. end
  27. end

lib/epub/parser/xml_document/refinements/oga.rb

64.86% lines covered

37 relevant lines. 24 lines covered and 13 lines missed.
    
  1. 1 require "oga"
  2. 1 module EPUB
  3. 1 class Parser
  4. 1 class XMLDocument
  5. 1 module Refinements
  6. 1 module Oga
  7. 1 [::Oga::XML::Document, ::Oga::XML::Node].each do |klass|
  8. 2 refine klass do
  9. [
  10. 2 [:document, ::Oga::XML::Document],
  11. [:element, ::Oga::XML::Element],
  12. [:text, ::Oga::XML::Text]
  13. ].each do |(type, klass)|
  14. 6 define_method "#{type}?" do
  15. kind_of? klass
  16. end
  17. end
  18. 2 def root
  19. # Couldn't use find(&:element?) for Rubies under 2.6
  20. root_node.children.find {|child| child.element?}
  21. end
  22. 2 def elements
  23. # Couldn't use find(&:element?) for Rubies under 2.6
  24. children.select {|child| child.element?}
  25. end
  26. # Need for Rubies under 2.6
  27. 2 def respond_to?(name, include_all = false)
  28. [:root, :elements].include?(name) || super
  29. end
  30. 2 def each_element_by_xpath(xpath, namespaces = nil, &block)
  31. xpath(xpath, namespaces: namespaces).each &block
  32. end
  33. end
  34. end
  35. 1 refine ::Oga::XML::Element do
  36. 1 def attribute_with_prefix(name, prefix = nil)
  37. name = prefix ? "#{prefix}:#{name}" : name
  38. get(name)
  39. end
  40. 1 def each_element(xpath = nil, &block)
  41. each_node do |node|
  42. throw :skip_children unless node.kind_of?(::Oga::XML::Element)
  43. block.call node
  44. end
  45. end
  46. 1 def namespace_uri
  47. namespace&.uri
  48. end
  49. 1 alias original_namespaces namespaces
  50. 1 def namespaces
  51. original_namespaces.each_with_object({}) {|(name, namespace), nss|
  52. nss[name] = namespace.uri
  53. }
  54. end
  55. 1 alias content text
  56. end
  57. 1 refine ::Oga::XML::Text do
  58. 1 alias content text
  59. end
  60. end
  61. 1 include Oga
  62. end
  63. end
  64. end
  65. end

lib/epub/parser/xml_document/refinements/rexml.rb

72.0% lines covered

25 relevant lines. 18 lines covered and 7 lines missed.
    
  1. 1 require "rexml/document"
  2. 1 module EPUB
  3. 1 class Parser
  4. 1 class XMLDocument
  5. 1 module Refinements
  6. 1 module REXML
  7. 1 [::REXML::Element, ::REXML::Text].each do |klass|
  8. 2 refine klass do
  9. 2 %i[document element text].each do |type|
  10. 6 define_method "#{type}?" do
  11. node_type == type
  12. end
  13. end
  14. end
  15. end
  16. 1 refine ::REXML::Element do
  17. 1 def each_element_by_xpath(xpath, namespaces = nil, &block)
  18. ::REXML::XPath.each self, xpath, namespaces, &block
  19. end
  20. 1 def attribute_with_prefix(name, prefix = nil)
  21. attribute(name, EPUB::NAMESPACES[prefix])&.value
  22. end
  23. 1 alias namespace_uri namespace
  24. 1 def content
  25. each_child.inject("") {|text, node|
  26. case node.node_type
  27. when :document, :element
  28. text << node.content
  29. when :text
  30. text << node.value
  31. end
  32. }
  33. end
  34. end
  35. 1 refine ::REXML::Text do
  36. 1 alias content value
  37. end
  38. end
  39. 1 include REXML
  40. end
  41. end
  42. end
  43. end

lib/epub/publication.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. 1 require 'epub/publication/package'
  2. 1 require 'epub/publication/fixed_layout'

lib/epub/publication/fixed_layout.rb

56.03% lines covered

116 relevant lines. 65 lines covered and 51 lines missed.
    
  1. 1 module EPUB
  2. 1 module Publication
  3. 1 module FixedLayout
  4. 1 PREFIX_KEY = 'rendition'.freeze
  5. 1 PREFIX_VALUE = 'http://www.idpf.org/vocab/rendition/#'.freeze
  6. RENDITION_PROPERTIES = {
  7. 1 'layout' => ['reflowable'.freeze, 'pre-paginated'.freeze].freeze,
  8. 'orientation' => ['auto'.freeze, 'landscape'.freeze, 'portrait'.freeze].freeze,
  9. 'spread' => ['auto'.freeze, 'none'.freeze, 'landscape'.freeze, 'portrait'.freeze, 'both'.freeze].freeze
  10. }.freeze
  11. 1 class UnsupportedRenditionValue < StandardError; end
  12. 1 class << self
  13. 1 def included(package_class)
  14. [
  15. [Package, PackageMixin],
  16. [Package::Metadata, MetadataMixin],
  17. [Package::Spine::Itemref, ItemrefMixin],
  18. [Package::Manifest::Item, ItemMixin],
  19. [ContentDocument::XHTML, ContentDocumentMixin],
  20. ].each do |(base, mixin)|
  21. base.__send__ :include, mixin
  22. end
  23. end
  24. end
  25. 1 module Rendition
  26. # @note Call after defining #rendition_xxx and #renditionn_xxx=
  27. 1 def def_rendition_methods
  28. 4 RENDITION_PROPERTIES.each_key do |property|
  29. 12 alias_method property, "rendition_#{property}"
  30. 12 alias_method "#{property}=", "rendition_#{property}="
  31. end
  32. 4 def_rendition_layout_methods
  33. end
  34. 1 def def_rendition_layout_methods
  35. 4 property = 'layout'
  36. 4 RENDITION_PROPERTIES[property].each do |value|
  37. 8 method_name_base = value.gsub('-', '_')
  38. 8 writer_name = "#{method_name_base}="
  39. 8 define_method writer_name do |new_value|
  40. new_prop = new_value ? value : values.find {|l| l != value}
  41. __send__ "rendition_#{property}=", new_prop
  42. end
  43. 8 maker_name = "make_#{method_name_base}"
  44. 8 define_method maker_name do
  45. __send__ "rendition_#{property}=", value
  46. end
  47. 8 destructive_method_name = "#{method_name_base}!"
  48. 8 alias_method destructive_method_name, maker_name
  49. 8 predicate_name = "#{method_name_base}?"
  50. 8 define_method predicate_name do
  51. __send__("rendition_#{property}") == value
  52. end
  53. end
  54. end
  55. end
  56. 1 module PackageMixin
  57. # @return [true, false]
  58. 1 def using_fixed_layout
  59. prefix.has_key? PREFIX_KEY and
  60. prefix[PREFIX_KEY] == PREFIX_VALUE
  61. end
  62. 1 alias using_fixed_layout? using_fixed_layout
  63. # @param using_fixed_layout [true, false]
  64. 1 def using_fixed_layout=(using_fixed_layout)
  65. if using_fixed_layout
  66. prefix[PREFIX_KEY] = PREFIX_VALUE
  67. else
  68. prefix.delete PREFIX_KEY
  69. end
  70. end
  71. end
  72. 1 module MetadataMixin
  73. 1 extend Rendition
  74. 1 RENDITION_PROPERTIES.each_pair do |property, values|
  75. 3 define_method "rendition_#{property}" do
  76. meta = metas.find {|m| m.property == "rendition:#{property}"}
  77. meta ? meta.content : values.first
  78. end
  79. 3 define_method "rendition_#{property}=" do |new_value|
  80. raise UnsupportedRenditionValue, new_value unless values.include? new_value
  81. prefixed_property = "rendition:#{property}"
  82. values_to_be_deleted = values - [new_value]
  83. metas.delete_if {|meta| meta.property == prefixed_property && values_to_be_deleted.include?(meta.content)}
  84. unless metas.any? {|meta| meta.property == prefixed_property && meta.content == new_value}
  85. meta = Package::Metadata::Meta.new
  86. meta.property = prefixed_property
  87. meta.content = new_value
  88. metas << meta
  89. end
  90. new_value
  91. end
  92. end
  93. 1 def_rendition_methods
  94. end
  95. 1 module ItemrefMixin
  96. 1 extend Rendition
  97. 1 PAGE_SPREAD_PROPERTY = 'center'
  98. 1 PAGE_SPREAD_PREFIX = 'rendition:page-spread-'
  99. 1 class << self
  100. # @todo Define using Module#prepend after Ruby 2.0 will become popular
  101. 1 def included(base)
  102. return if base.instance_methods.include? :page_spread_without_fixed_layout
  103. base.__send__ :alias_method, :page_spread_without_fixed_layout, :page_spread
  104. base.__send__ :alias_method, :page_spread_writer_without_fixed_layout, :page_spread=
  105. prefixed_page_spread_property = "#{PAGE_SPREAD_PREFIX}#{PAGE_SPREAD_PROPERTY}"
  106. base.__send__ :define_method, :page_spread do
  107. property = page_spread_without_fixed_layout
  108. return property if property
  109. properties.include?(prefixed_page_spread_property) ? PAGE_SPREAD_PROPERTY : nil
  110. end
  111. base.__send__ :define_method, :page_spread= do |new_value|
  112. if new_value == PAGE_SPREAD_PROPERTY
  113. page_spread_writer_without_fixed_layout nil
  114. properties << prefixed_page_spread_property
  115. else
  116. page_spread_writer_without_fixed_layout new_value
  117. end
  118. new_value
  119. end
  120. end
  121. end
  122. 1 RENDITION_PROPERTIES.each do |property, values|
  123. 3 rendition_property_prefix = "rendition:#{property}-"
  124. 3 reader_name = "rendition_#{property}"
  125. 3 define_method reader_name do
  126. prop_value = properties.find {|prop| prop.start_with? rendition_property_prefix}
  127. prop_value ? prop_value.gsub(/\A#{Regexp.escape(rendition_property_prefix)}/, '') :
  128. spine.package.metadata.__send__(reader_name)
  129. end
  130. 3 writer_name = "#{reader_name}="
  131. 3 define_method writer_name do |new_value|
  132. if new_value.nil?
  133. properties.delete_if {|prop| prop.start_with? rendition_property_prefix}
  134. return new_value
  135. end
  136. raise UnsupportedRenditionValue, new_value unless values.include? new_value
  137. values_to_be_deleted = (values - [new_value]).map {|value| "#{rendition_property_prefix}#{value}"}
  138. properties.delete_if {|prop| values_to_be_deleted.include? prop}
  139. new_property = "#{rendition_property_prefix}#{new_value}"
  140. properties << new_property unless properties.include? new_property
  141. new_value
  142. end
  143. end
  144. 1 def_rendition_methods
  145. end
  146. 1 module ItemMixin
  147. 1 extend Rendition
  148. 1 RENDITION_PROPERTIES.each_key do |property|
  149. 3 define_method "rendition_#{property}" do
  150. itemref.__send__ property
  151. end
  152. 3 writer_name = "rendition_#{property}="
  153. 3 define_method writer_name do |value|
  154. itemref.__send__ writer_name, value
  155. end
  156. end
  157. 1 def_rendition_methods
  158. end
  159. 1 module ContentDocumentMixin
  160. 1 extend Rendition
  161. 1 RENDITION_PROPERTIES.each_key do |property|
  162. 3 reader_name = "rendition_#{property}"
  163. 3 define_method reader_name do
  164. item.__send__ reader_name
  165. end
  166. 3 writer_name = "rendition_#{property}="
  167. 3 define_method writer_name do |value|
  168. item.__send__ writer_name, value
  169. end
  170. end
  171. 1 def_rendition_methods
  172. end
  173. end
  174. end
  175. end

lib/epub/publication/package.rb

62.16% lines covered

37 relevant lines. 23 lines covered and 14 lines missed.
    
  1. 1 module EPUB
  2. 1 module Publication
  3. 1 class Package
  4. 1 include Inspector
  5. 1 CONTENT_MODELS = [:metadata, :manifest, :spine, :guide, :bindings]
  6. RESERVED_VOCABULARY_PREFIXES = {
  7. 1 '' => 'http://idpf.org/epub/vocab/package/#',
  8. 'dcterms' => 'http://purl.org/dc/terms/',
  9. 'marc' => 'http://id.loc.gov/vocabulary/',
  10. 'media' => 'http://www.idpf.org/epub/vocab/overlays/#',
  11. 'onix' => 'http://www.editeur.org/ONIX/book/codelists/current.html#',
  12. 'xsd' => 'http://www.w3.org/2001/XMLSchema#'
  13. }
  14. 1 class << self
  15. 1 def define_content_model(model_name)
  16. 5 define_method "#{model_name}=" do |model|
  17. current_model = __send__(model_name)
  18. current_model.package = nil if current_model
  19. model.package = self
  20. instance_variable_set "@#{model_name}", model
  21. end
  22. end
  23. end
  24. 1 attr_accessor :book,
  25. :version, :prefix, :xml_lang, :dir, :id
  26. 1 attr_reader *CONTENT_MODELS
  27. 1 alias lang xml_lang
  28. 1 alias lang= xml_lang=
  29. 1 CONTENT_MODELS.each do |model|
  30. 5 define_content_model model
  31. end
  32. 1 def initialize
  33. @prefix = {}
  34. end
  35. # @return [EPUB::Metadata::Identifier] Unique Identifier
  36. 1 def unique_identifier
  37. @metadata.unique_identifier
  38. end
  39. # Corresponding {Rootfile}
  40. # @return [OCF::Container::Rootfile]
  41. 1 def rootfile
  42. @book.ocf.container.rootfiles.find {|rf| rf.package == self}
  43. end
  44. # Full path in EPUB archive
  45. # @return [Addressable::URI]
  46. 1 def full_path
  47. rootfile.full_path if rootfile
  48. end
  49. 1 def inspect
  50. "#<%{class}:%{object_id} %{attributes} %{models}>" % {
  51. :class => self.class,
  52. :object_id => inspect_object_id,
  53. :attributes => inspect_instance_variables(exclude: CONTENT_MODELS.map {|model| :"@#{model}"}),
  54. :models => inspect_models
  55. }
  56. end
  57. 1 def inspect_models
  58. CONTENT_MODELS.map {|name|
  59. model = __send__(name)
  60. representation = model.nil? ? model.inspect : model.inspect_simply
  61. "@#{name}=#{representation}"
  62. }.join(' ')
  63. end
  64. end
  65. end
  66. end
  67. 1 EPUB::Publication::Package::CONTENT_MODELS.each do |f|
  68. 5 require_relative "package/#{f}"
  69. end

lib/epub/publication/package/bindings.rb

70.59% lines covered

17 relevant lines. 12 lines covered and 5 lines missed.
    
  1. 1 module EPUB
  2. 1 module Publication
  3. 1 class Package
  4. 1 class Bindings
  5. 1 include Inspector::PublicationModel
  6. 1 attr_accessor :package
  7. 1 def initialize
  8. @media_types = {}
  9. end
  10. 1 def <<(media_type)
  11. @media_types[media_type.media_type] = media_type
  12. end
  13. 1 def [](media_type)
  14. _, mt = @media_types.detect {|key, _| key == media_type}
  15. mt
  16. end
  17. 1 def media_types
  18. @media_types.values
  19. end
  20. 1 class MediaType
  21. 1 attr_accessor :media_type, :handler
  22. end
  23. end
  24. end
  25. end
  26. end

lib/epub/publication/package/guide.rb

53.13% lines covered

32 relevant lines. 17 lines covered and 15 lines missed.
    
  1. 1 module EPUB
  2. 1 module Publication
  3. 1 class Package
  4. 1 class Guide
  5. 1 include Inspector::PublicationModel
  6. 1 attr_accessor :package, :references
  7. 1 def initialize
  8. Reference::TYPES.each do |type|
  9. variable_name = '@' + type.gsub('-', '_')
  10. instance_variable_set variable_name, nil
  11. end
  12. @references = []
  13. end
  14. 1 def <<(reference)
  15. reference.guide = self
  16. references << reference
  17. end
  18. 1 class Reference
  19. 1 TYPES = %w[cover title-page toc index glossary acknowledgements bibliography colophon copyright-page dedication epigraph foreword loi lot notes preface text]
  20. 1 attr_accessor :guide,
  21. :type, :title
  22. 1 attr_reader :href
  23. 1 def href=(iri)
  24. @href = iri.kind_of?(Addressable::URI) ? iri : Addressable::URI.parse(iri)
  25. end
  26. 1 def item
  27. return @item if @item
  28. request_uri = href.request_uri
  29. @item = @guide.package.manifest.items.find {|item|
  30. item.href.request_uri == request_uri
  31. }
  32. end
  33. end
  34. 1 Reference::TYPES.each do |type|
  35. 17 method_name = type.gsub('-', '_')
  36. 17 define_method method_name do
  37. var = instance_variable_get "@#{method_name}"
  38. return var if var
  39. var = references.find {|ref| ref.type == type}
  40. instance_variable_set "@#{method_name}", var
  41. end
  42. end
  43. end
  44. end
  45. end
  46. end

lib/epub/publication/package/manifest.rb

37.5% lines covered

112 relevant lines. 42 lines covered and 70 lines missed.
    
  1. 1 require 'set'
  2. 1 require 'addressable/uri'
  3. 1 require 'rchardet'
  4. 1 require 'epub/constants'
  5. 1 require 'epub/parser/content_document'
  6. 1 module EPUB
  7. 1 module Publication
  8. 1 class Package
  9. 1 class Manifest
  10. 1 include Inspector::PublicationModel
  11. 1 attr_accessor :package,
  12. :id
  13. 1 def initialize
  14. @items = {}
  15. end
  16. # @param item [Item]
  17. # @return [Manifest] self
  18. 1 def <<(item)
  19. item.manifest = self
  20. @items[item.id] = item
  21. self
  22. end
  23. 1 def each_nav
  24. if block_given?
  25. each_item do |item|
  26. yield item if item.nav?
  27. end
  28. else
  29. each_item.lazy.select(&:nav?)
  30. end
  31. end
  32. # @return [Array<Item>] item which includes "nav" as one of +properties+. It represents this item is a navigation of book.
  33. 1 def navs
  34. items.select(&:nav?)
  35. end
  36. # @return [Item, nil] the first item of #navs
  37. 1 def nav
  38. navs.first
  39. end
  40. # @return [Item, nil] item which includes "cover-image" as one of +properties+. It represents this item is cover image.
  41. 1 def cover_image
  42. items.find(&:cover_image?)
  43. end
  44. # @overload each_item
  45. # @yield [item]
  46. # @yieldparam [Item]
  47. # @overload each_item
  48. # @return [Enumerator]
  49. 1 def each_item
  50. if block_given?
  51. @items.each_value do |item|
  52. yield item
  53. end
  54. else
  55. @items.each_value
  56. end
  57. end
  58. 1 def items
  59. @items.values
  60. end
  61. # @param item_id [String]
  62. # @return [Item, nil]
  63. 1 def [](item_id)
  64. @items[item_id]
  65. end
  66. 1 class Item
  67. 1 DUMMY_ROOT_IRI = Addressable::URI.parse('http://example.net/').freeze
  68. 1 include Inspector
  69. # @!attribute [rw] manifest
  70. # @return [Manifest] Returns the value of manifest
  71. # @!attribute [rw] id
  72. # @return [String] Returns the value of id
  73. # @!attribute [rw] href
  74. # @return [Addressable::URI] Returns the value of href,
  75. # which is relative IRI from rootfile(OPF file)
  76. # @!attribute [rw] media_type
  77. # @return [String] Returns the value of media_type
  78. # @!attribute [rw] properties
  79. # @return [Set<String>] Returns the value of properties
  80. # @!attribute [rw] media_overlay
  81. # @return [String] Returns the value of media_overlay
  82. # @!attribute [rw] fallback
  83. # @return [Item] Returns the value of attribute fallback
  84. 1 attr_accessor :manifest,
  85. :id, :media_type, :fallback, :media_overlay
  86. 1 attr_reader :properties, :href
  87. 1 def initialize
  88. @properties = Set.new
  89. @full_path = nil
  90. end
  91. 1 def properties=(props)
  92. @properties = props.kind_of?(Set) ? props : Set.new(props)
  93. end
  94. 1 def href=(iri)
  95. @href = iri.kind_of?(Addressable::URI) ? iri : Addressable::URI.parse(iri)
  96. end
  97. # @todo Handle circular fallback chain
  98. 1 def fallback_chain
  99. @fallback_chain ||= traverse_fallback_chain([])
  100. end
  101. # full path in archive
  102. # @return [Addressable::URI]
  103. 1 def full_path
  104. return @full_path if @full_path
  105. path = DUMMY_ROOT_IRI + manifest.package.full_path + href
  106. path.scheme = nil
  107. path.host = nil
  108. path.path = path.path[1..-1]
  109. @full_path = path
  110. end
  111. # full path in archive
  112. # @return [String]
  113. 1 def entry_name
  114. Addressable::URI.unencode(full_path)
  115. end
  116. # Read content from EPUB archive
  117. #
  118. # @param detect_encoding [Boolean] Whether #read tries auto-detection of character encoding. The default value is +false+.
  119. # @return [String] Content with encoding:
  120. # US-ASCII when the content is not in text format such images.
  121. # UTF-8 when the content is in text format and +detect_encoding+ is +false+.
  122. # auto-detected encoding when the content is in text format and +detect_encoding+ is +true+.
  123. 1 def read(detect_encoding: false)
  124. raw_content = manifest.package.book.container_adapter.read(manifest.package.book.epub_file, entry_name)
  125. unless media_type.start_with?('text/') or
  126. media_type.end_with?('xml') or
  127. ['application/json', 'application/javascript', 'application/ecmascript', 'application/xml-dtd'].include?(media_type)
  128. return raw_content
  129. end
  130. if detect_encoding
  131. # CharDet.detect doesn't raise Encoding::CompatibilityError
  132. # that is caused when trying compare CharDet's internal
  133. # ASCII-8BIT RegExp with a String with other encoding
  134. # because Zip::File#read returns a String with encoding ASCII-8BIT.
  135. # So, no need to rescue the error here.
  136. encoding = CharDet.detect(raw_content)['encoding']
  137. if encoding
  138. raw_content.force_encoding(encoding)
  139. else
  140. warn "No encoding detected for #{entry_name}. Set to ASCII-8BIT" if $DEBUG || $VERBOSE
  141. raw_content
  142. end
  143. else
  144. raw_content.force_encoding("UTF-8");
  145. end
  146. end
  147. 1 def xhtml?
  148. media_type == 'application/xhtml+xml'
  149. end
  150. 1 def nav?
  151. properties.include? 'nav'
  152. end
  153. 1 def cover_image?
  154. properties.include? 'cover-image'
  155. end
  156. # @todo Handle circular fallback chain
  157. 1 def use_fallback_chain(options = {})
  158. supported = EPUB::MediaType::CORE
  159. if ad = options[:supported]
  160. supported = supported | (ad.respond_to?(:to_ary) ? ad : [ad])
  161. end
  162. if del = options[:unsupported]
  163. supported = supported - (del.respond_to?(:to_ary) ? del : [del])
  164. end
  165. return yield self if supported.include? media_type
  166. if (bindings = manifest.package.bindings) && (binding_media_type = bindings[media_type])
  167. return yield binding_media_type.handler
  168. end
  169. return fallback.use_fallback_chain(options) {|fb| yield fb} if fallback
  170. raise EPUB::MediaType::UnsupportedMediaType
  171. end
  172. 1 def content_document
  173. return nil unless %w[application/xhtml+xml image/svg+xml].include? media_type
  174. @content_document ||= Parser::ContentDocument.new(self).parse
  175. end
  176. # @return [Package::Spine::Itemref]
  177. # @return nil when no Itemref refers this Item
  178. 1 def itemref
  179. manifest.package.spine.itemrefs.find {|itemref| itemref.idref == id}
  180. end
  181. # @param iri [Addressable::URI] relative iri
  182. # @return [Item]
  183. # @return [nil] when item not found
  184. # @raise ArgumentError when +iri+ is not relative
  185. # @raise ArgumentError when +iri+ starts with "/"(slash)
  186. # @note Algorithm stolen form Rack::Utils#clean_path_info
  187. 1 def find_item_by_relative_iri(iri)
  188. raise ArgumentError, "Not relative: #{iri.inspect}" unless iri.relative?
  189. raise ArgumentError, "Start with slash: #{iri.inspect}" if iri.path.start_with? Addressable::URI::SLASH
  190. target_href = href + iri
  191. target_href.fragment = nil
  192. segments = target_href.to_s.split(Addressable::URI::SLASH)
  193. clean_segments = []
  194. segments.each do |segment|
  195. next if segment.empty? || segment == '.'
  196. segment == '..' ? clean_segments.pop : clean_segments << segment
  197. end
  198. target_iri = Addressable::URI.parse(clean_segments.join(Addressable::URI::SLASH))
  199. manifest.items.find { |item| item.href == target_iri}
  200. end
  201. 1 def inspect
  202. "#<%{class}:%{object_id} %{manifest} %{attributes}>" % {
  203. :class => self.class,
  204. :object_id => inspect_object_id,
  205. :manifest => "@manifest=#{@manifest.inspect_simply}",
  206. :attributes => inspect_instance_variables(exclude: [:@manifest])
  207. }
  208. end
  209. 1 protected
  210. 1 def traverse_fallback_chain(chain)
  211. chain << self
  212. return chain unless fallback
  213. fallback.traverse_fallback_chain(chain)
  214. end
  215. end
  216. end
  217. end
  218. end
  219. end

lib/epub/publication/package/metadata.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. 1 require 'epub/metadata'
  2. 1 module EPUB
  3. 1 module Publication
  4. 1 class Package
  5. 1 Metadata = EPUB::Metadata
  6. end
  7. end
  8. end

lib/epub/publication/package/spine.rb

46.3% lines covered

54 relevant lines. 25 lines covered and 29 lines missed.
    
  1. 1 require 'set'
  2. 1 module EPUB
  3. 1 module Publication
  4. 1 class Package
  5. 1 class Spine
  6. 1 include Inspector::PublicationModel
  7. 1 attr_accessor :package,
  8. :id, :toc, :page_progression_direction
  9. 1 attr_reader :itemrefs
  10. 1 def initialize
  11. @itemrefs = []
  12. end
  13. # @return self
  14. 1 def <<(itemref)
  15. itemref.spine = self
  16. @itemrefs << itemref
  17. self
  18. end
  19. # @yield [itemref]
  20. # @yieldparam [Itemref] itemref
  21. # @yieldreturn [Object] returns the last value of block
  22. # @return [Object, Enumerator]
  23. # returns the last value of block when block given, Enumerator when not
  24. 1 def each_itemref
  25. if block_given?
  26. itemrefs.each {|itemref| yield itemref}
  27. else
  28. enum_for :each_itemref
  29. end
  30. end
  31. # @return [Enumerator] Enumerator which yeilds {Manifest::Item}
  32. # referred by each of {#itemrefs}
  33. 1 def items
  34. itemrefs.collect {|itemref| itemref.item}
  35. end
  36. 1 class Itemref
  37. 1 PAGE_SPREAD_PROPERTIES = ['left'.freeze, 'right'.freeze].freeze
  38. 1 PAGE_SPREAD_PREFIX = 'page-spread-'.freeze
  39. 1 attr_accessor :spine,
  40. :idref, :linear, :id
  41. 1 attr_reader :properties
  42. 1 def initialize
  43. @properties = Set.new
  44. end
  45. 1 def properties=(props)
  46. @properties = props.kind_of?(Set) ? props : Set.new(props)
  47. end
  48. # @return [true|false]
  49. 1 def linear?
  50. !! linear
  51. end
  52. # @return [Package::Manifest::Item] item referred by this object
  53. 1 def item
  54. @item ||= @spine.package.manifest[idref]
  55. end
  56. 1 def item=(item)
  57. self.idref = item.id
  58. item
  59. end
  60. 1 def ==(other)
  61. [:spine, :idref, :id].all? {|meth|
  62. self.__send__(meth) == other.__send__(meth)
  63. } and
  64. (linear? == other.linear?) and
  65. (properties == other.properties)
  66. end
  67. # @return ["left", "right", nil]
  68. 1 def page_spread
  69. property = properties.find {|prop| prop.start_with? PAGE_SPREAD_PREFIX}
  70. property ? property.gsub(/\A#{Regexp.escape(PAGE_SPREAD_PREFIX)}/, '') : nil
  71. end
  72. # @param new_value ["left", "right", nil]
  73. 1 def page_spread=(new_value)
  74. if new_value.nil?
  75. properties.delete_if {|prop| prop.start_with? PAGE_SPREAD_PREFIX}
  76. return new_value
  77. end
  78. raise "Unsupported page-spread property: #{new_value}" unless PAGE_SPREAD_PROPERTIES.include? new_value
  79. props_to_be_deleted = (PAGE_SPREAD_PROPERTIES - [new_value]).map {|prop| "#{PAGE_SPREAD_PREFIX}#{prop}"}
  80. properties.delete_if {|prop| props_to_be_deleted.include? prop}
  81. new_property = "#{PAGE_SPREAD_PREFIX}#{new_value}"
  82. properties << new_property unless properties.include? new_property
  83. new_value
  84. end
  85. end
  86. end
  87. end
  88. end
  89. end