All Files ( 50.41% covered at 0.77 hits/line )
39 files in total.
1228 relevant lines,
619 lines covered and
609 lines missed.
(
50.41%
)
- 1
require 'epub/inspector'
- 1
require 'epub/ocf'
- 1
require 'epub/publication'
- 1
require 'epub/content_document'
- 1
require 'epub/book/features'
- 1
require 'epub'
- 1
module EPUB
- 1
class Book
- 1
include EPUB::Book::Features
end
end
- 1
require 'forwardable'
- 1
module EPUB
- 1
class Book
- 1
module Features
- 1
extend Forwardable
- 1
attr_reader :ocf
- 1
attr_accessor :epub_file
# When writing, sets +ocf.book+ to self.
# @param [OCF]
- 1
def ocf=(mod)
@ocf = mod
mod.book = self
mod
end
# @return [Array<OCF::Container::Rootfile>]
- 1
def rootfiles
ocf.container.rootfiles
end
# @return [Array<Publication::Package>]
- 1
def packages
rootfiles.map(&:package)
end
- 1
alias renditions packages
# First +package+ in +packages+
# @return [Package|nil]
- 1
def default_rendition
packages.first
end
- 1
alias package default_rendition
# @!parse def_delegators :package, :metadata, :manifest, :spine, :guide, :bindings
- 1
def_delegators :package, *Publication::Package::CONTENT_MODELS
- 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
- 1
def_delegators :manifest, :nav
- 1
def container_adapter
@adapter || OCF::PhysicalContainer.adapter
end
- 1
def container_adapter=(adapter)
@adapter = OCF::PhysicalContainer.find_adapter(adapter)
end
# Cover image defined in EPUB 3 or used in EPUB 2
# @return [EPUB::Publication::Package::Manifest::Item]
- 1
def cover_image
manifest.cover_image || metadata.cover_image
end
# @overload each_page_on_spine(&blk)
# iterate over items in order of spine when block given
# @yieldparam item [Publication::Package::Manifest::Item]
# @overload each_page_on_spine
# @return [Enumerator] which iterates over {Publication::Package::Manifest::Item}s in order of spine when block not given
- 1
def each_page_on_spine(&blk)
enum = package.spine.items
if block_given?
enum.each &blk
else
enum.each
end
end
- 1
def each_page_on_toc(&blk)
raise NotImplementedError
end
# @overload each_content(&blk)
# iterate all items over when block given
# @yieldparam item [Publication::Package::Manifest::Item]
# @overload each_content
# @return [Enumerator] which iterates over all {Publication::Package::Manifest::Item}s in EPUB package when block not given
- 1
def each_content(&blk)
enum = manifest.items
if block_given?
enum.each &blk
else
enum.to_enum
end
end
- 1
def other_navigation
raise NotImplementedError
end
# @return [Array<Publication::Package::Manifest::Item>] All {Publication::Package::Manifest::Item}s in EPUB package
- 1
def resources
manifest.items
end
# Syntax sugar
# @return String
- 1
def rootfile_path
ocf.container.rootfile.full_path.to_s
end
end
end
end
- 1
module EPUB
NAMESPACES = {
- 1
'xml' => 'http://www.w3.org/XML/1998/namespace',
'dc' => 'http://purl.org/dc/elements/1.1/',
'ocf' => 'urn:oasis:names:tc:opendocument:xmlns:container',
'opf' => 'http://www.idpf.org/2007/opf',
'xhtml' => 'http://www.w3.org/1999/xhtml',
'epub' => 'http://www.idpf.org/2007/ops',
'm' => 'http://www.w3.org/1998/Math/MathML',
'svg' => 'http://www.w3.org/2000/svg',
'smil' => 'http://www.w3.org/ns/SMIL',
'metadata' => 'http://www.idpf.org/2013/metadata'
}
- 1
module MediaType
- 1
class UnsupportedMediaType < StandardError; end
- 1
EPUB = 'application/epub+zip'
- 1
ROOTFILE = 'application/oebps-package+xml'
IMAGE = %w[
- 1
image/gif
image/jpeg
image/png
image/svg+xml
]
APPLICATION = %w[
- 1
application/xhtml+xml
application/x-dtbncx+xml
application/vnd.ms-opentype
application/font-woff
application/smil+xml
application/pls+xml
]
AUDIO = %w[
- 1
audio/mpeg
audio/mp4
]
TEXT = %w[
- 1
text/css
text/javascript
]
- 1
CORE = IMAGE + APPLICATION + AUDIO + TEXT
end
# @see https://idpf.github.io/epub-vocabs/structure/
- 1
module Type
DOCUMENT_NAVIGATION = %w[
- 1
toc
landmarks
]
PAGINATION = %w[
- 1
pagebreak
page_list
]
- 1
TYPES = DOCUMENT_NAVIGATION + PAGINATION
end
end
- 1
require 'epub/content_document/xhtml'
- 1
require 'epub/content_document/navigation'
- 1
require "epub/content_document/typable"
- 1
module EPUB
- 1
module ContentDocument
- 1
class Navigation < XHTML
- 1
include Typable
- 1
attr_accessor :navigations
- 1
def initialize
@navigations = []
@hidden = false
@parent = nil
super
end
- 1
def toc
navigations.find(&:toc?)
end
- 1
def page_list
navigations.find(&:page_list?)
end
- 1
def landmarks
navigations.find(&:landmarks?)
end
# Enumerator version of toc
- 1
def contents
enum_for(:each_content).to_a
end
# Enumerator version of page_list
# Usage: navigation.enum_for(:pages)
- 1
def pages
raise NotImplementedError
end
# @todo Enumerator version of landmarks
# iterator for #toc
- 1
def each_content
toc.traverse do |content, _|
yield content
end
end
# iterator for #page_list
- 1
def each_page
raise NotImplementedError
end
# iterator for #landmark
- 1
def each_landmark
raise NotImplementedError
end
- 1
def navigation
navigations.first
end
- 1
module Hidable
- 1
attr_accessor :hidden, :parent
- 1
def hidden?
if @hidden.nil?
@parent ? @parent.hidden? : false
else
true
end
end
end
- 1
class Item
- 1
include Hidable
- 1
include Typable
- 1
attr_accessor :items, :text,
:content_document, :item
- 1
attr_reader :href
- 1
def initialize
@items = ItemList.new
@items.parent = self
end
- 1
def href=(iri)
@href = iri.kind_of?(Addressable::URI) ? iri : Addressable::URI.parse(iri)
end
- 1
def traverse(depth=0, &block)
block.call self, depth
items.each do |item|
item.traverse depth + 1, &block
end
end
# For backward compatibility
- 1
def type
types.find {|t|
Type::TYPES.include? t
}
end
# For backward compatibility
- 1
def type=(t)
types << t
end
- 1
%w[toc page_list landmarks].each do |type|
- 3
define_method "#{type}?" do
types.include? type
end
end
end
# @todo Implement method to represent navigation structure
- 1
class Navigation < Item
- 1
module Type
- 1
TOC = 'toc'
- 1
PAGE_LIST = 'page_list'
- 1
LANDMARKS = 'landmarks'
end
- 1
alias navigations items
- 1
alias navigations= items=
- 1
alias heading text
- 1
alias heading= text=
end
- 1
class ItemList < Array
- 1
include Hidable
- 1
def <<(item)
super
item.parent = self
end
end
end
end
end
- 1
require "set"
- 1
module EPUB
- 1
module ContentDocument
- 1
module Typable
- 1
def types
@types ||= Set.new
end
- 1
def types=(ts)
@types = ts.kind_of?(Set) ? ts : Set.new(ts)
end
end
end
end
- 1
module EPUB
- 1
module ContentDocument
- 1
class XHTML
- 1
attr_accessor :item
# @param [Boolean] detect_encoding See {Publication::Package::Manifest::Item#read}
# @return [String] Returns the content string.
- 1
def read(detect_encoding: true)
item.read(detect_encoding: detect_encoding)
end
- 1
alias raw_document read
# @return [true|false] Whether referenced directly from spine or not.
- 1
def top_level?
!! item.itemref
end
# @return [String] Returns the value of title element.
# If none, returns empty string
- 1
def title
title_elem = rexml.get_elements('.//title').first
if title_elem
title_elem.text
else
warn 'title element not found'
''
end
end
# @return [REXML::Document] content as REXML::Document object
- 1
def rexml
@rexml ||= REXML::Document.new(raw_document)
end
# @return [Oga::XML::Document] content as Oga::XML::Document object
- 1
def oga
require "oga"
@oga ||= Oga.parse_xml(raw_document)
end
# @return [Nokogiri::XML::Document] content as Nokogiri::XML::Document object
- 1
def nokogiri
require 'nokogiri'
@nokogiri ||= Nokogiri.XML(raw_document)
end
end
end
end
- 1
module EPUB
- 1
module Inspector
- 1
INSTANCE_VARIABLES_OPTION = {:exclude => []}
- 1
SIMPLE_TEMPLATE = "#<%{class}:%{object_id}>"
- 1
def inspect_simply
SIMPLE_TEMPLATE % {
:class => self.class,
:object_id => inspect_object_id
}
end
- 1
def inspect_object_id
(__id__ << 1).to_s(16)
end
- 1
def inspect_instance_variables(options={})
options = INSTANCE_VARIABLES_OPTION.merge(options)
exclude = options[:exclude]
(instance_variables - exclude).map {|name|
value = instance_variable_get(name)
"#{name}=#{value.inspect}"
}.join(' ')
end
- 1
module PublicationModel
- 1
class << self
- 1
def included(mod)
- 5
mod.__send__ :include, Inspector
end
end
- 1
def inspect
template % {
:class => self.class,
:package => (package && package.inspect_simply),
:object_id => inspect_object_id,
:attributes => inspect_instance_variables(exclude: [:@package])
}
end
- 1
def template
t = "#<%{class}:%{object_id}"
t << " @package=%{package}" if package
t << " %{attributes}>"
end
end
end
end
- 1
require 'set'
- 1
module EPUB
- 1
class Metadata
- 1
include Inspector::PublicationModel
- 1
DC_ELEMS = [:identifiers, :titles, :languages] +
[:contributors, :coverages, :creators, :dates, :descriptions, :formats, :publishers,
:relations, :rights, :sources, :subjects, :types]
# Used for CFI
- 1
attr_reader :children
- 1
attr_accessor :package, :unique_identifier, :metas, :links,
- 16
*(DC_ELEMS.collect {|elem| "dc_#{elem}"})
- 1
DC_ELEMS.each do |elem|
- 15
alias_method elem, "dc_#{elem}"
- 15
alias_method "#{elem}=", "dc_#{elem}="
end
- 1
def initialize
(DC_ELEMS + [:metas, :links]).each do |elem|
__send__ "#{elem}=", []
end
@children = []
end
- 1
def release_identifier
"#{unique_identifier}@#{modified}"
end
- 1
alias package_identifier release_identifier
- 1
def title
return extended_title unless extended_title.empty?
compositted = titles.select {|title| title.display_seq}.sort.join("\n")
return compositted unless compositted.empty?
return main_title unless main_title.empty?
titles.sort.join("\n")
end
- 1
%w[main short collection edition extended].each do |type|
- 5
define_method "#{type}_title" do
titles.select {|title| title.title_type.to_s == type}.sort.join(' ')
end
end
- 1
def subtitle
titles.select {|title| title.title_type.to_s == 'subtitle'}.sort.join(' ')
end
- 1
def description
descriptions.join(' ')
end
- 1
def date
dates.first
end
- 1
def language
languages.first
end
- 1
def modified
metas.find {|meta|
meta.property == 'dcterms:modified' &&
meta.refiners.empty?
}
end
# Cover image used in EPUB 2
# @return [EPUB::Publication::Package::Manifest::Item]
- 1
def cover_image
cover_image_meta = metas.find {|meta| meta.name == "cover"}
return unless cover_image_meta
package.manifest[cover_image_meta.meta_content]
end
- 1
def to_h
DC_ELEMS.inject({}) do |hsh, elem|
hsh[elem] = __send__(elem)
hsh
end
end
- 1
def primary_metas
metas.select {|meta| meta.primary_expression?}
end
- 1
module Refinee
- 1
PROPERTIES = %w[alternate-script display-seq file-as group-position identifier-type meta-auth role title-type]
- 1
attr_writer :refiners
- 1
def refiners
@refiners ||= Set.new
end
- 1
PROPERTIES.each do |voc|
- 8
met = voc.gsub(/-/, '_')
- 8
attr_writer met
- 8
define_method met do
refiners.find {|refiner| refiner.property == voc}
end
end
end
- 1
class DCMES
- 1
include Refinee
- 1
attr_accessor :content, :id, :lang, :dir
- 1
def to_s
content.to_s
end
end
- 1
class Identifier < DCMES
# @note This is ad-hoc
# @todo Define and include OPF module for opf:scheme attribute
# @todo Define general way to handle with identifier-type refiners
- 1
attr_accessor :scheme
# @note This is ad-hoc
# @todo Define and include OPF module for opf:scheme attribute
# @todo Define general way to handle with identifier-type refiners
- 1
def isbn?
scheme == 'ISBN' or
content.to_s.downcase.start_with? 'urn:isbn' or
refiners.any? {|refiner|
refiner.property == 'identifier-type' and
refiner.scheme == 'onix:codelist5' and
%w[02 15].include? refiner.content
}
end
end
- 1
class Title < DCMES
- 1
include Comparable
- 1
def <=>(other)
return 1 if other.display_seq.nil?
return -1 if display_seq.nil?
display_seq.to_s.to_i <=> other.display_seq.to_s.to_i
end
end
- 1
class Meta
- 1
include Refinee
- 1
attr_accessor :property, :id, :scheme, :content, :name, :meta_content
- 1
attr_reader :refines
- 1
def refines=(refinee)
refinee.refiners << self
@refines = refinee
end
- 1
def refines?
! refines.nil?
end
- 1
alias subexpression? refines?
- 1
def primary_expression?
! subexpression?
end
- 1
def inspect
ivs = instance_variables.map {|iv|
[iv, instance_variable_get(iv).inspect].join('=')
}.join(' ')
'<#%s:%#0x %s>' % [self.class, __id__, ivs]
end
- 1
def to_s
content.to_s
end
end
- 1
class Link
- 1
include Refinee
- 1
attr_accessor :href, :rel, :id, :media_type
- 1
attr_reader :refines
- 1
def refines=(refinee)
refinee.refiners << self
@refines = refinee
end
end
- 1
class UnsupportedModel
- 1
attr_accessor :raw_element
- 1
def initialize(raw_element)
@raw_element = raw_element
end
end
end
end
- 1
module EPUB
- 1
class OCF
- 1
MODULES = %w[container encryption manifest metadata rights signatures]
- 7
MODULES.each {|m| require "epub/ocf/#{m}"}
- 1
attr_accessor :book, *MODULES
end
end
- 1
module EPUB
- 1
class OCF
- 1
class Container
- 1
FILE = 'container.xml'
- 1
attr_reader :rootfiles
- 1
def initialize
@rootfiles = []
end
# syntax sugar
- 1
def rootfile
rootfiles.first
end
- 1
class Rootfile
- 1
attr_accessor :full_path, :media_type,
:package
# @param full_path [Addressable::URI|nil]
# @param media_type [String]
- 1
def initialize(full_path=nil, media_type=EPUB::MediaType::ROOTFILE)
@full_path, @media_type = full_path, media_type
end
end
end
end
end
- 1
module EPUB
- 1
class OCF
- 1
class Encryption
- 1
attr_accessor :content
end
end
end
- 1
module EPUB
- 1
class OCF
- 1
class Manifest
end
end
end
- 1
module EPUB
- 1
class OCF
- 1
class UnknownFormatMetadata
- 1
attr_accessor :content
end
end
end
- 1
require 'monitor'
- 1
require 'epub/ocf/physical_container/archive_zip'
- 1
require 'epub/ocf/physical_container/unpacked_directory'
- 1
require 'epub/ocf/physical_container/unpacked_uri'
- 1
module EPUB
- 1
class OCF
- 1
class PhysicalContainer
- 1
class NoEntry < StandardError
- 1
class << self
- 1
def from_error(error)
no_entry = new(error.message)
no_entry.set_backtrace error.backtrace
no_entry
end
end
end
- 1
@adapter = ArchiveZip
- 1
class << self
- 1
def find_adapter(adapter)
return adapter if adapter.instance_of? Class
if adapter == :Zipruby && ! const_defined?(adapter)
require 'epub/ocf/physical_container/zipruby'
end
const_get adapter
end
- 1
def adapter
raise NoMethodError, "undefined method `#{__method__}' for #{self}" unless self == PhysicalContainer
@adapter
end
- 1
def adapter=(adapter)
raise NoMethodError, "undefined method `#{__method__}' for #{self}" unless self == PhysicalContainer
@adapter = find_adapter(adapter)
end
- 1
def open(container_path)
- 2
_adapter.new(container_path).open do |container|
- 2
yield container
end
end
- 1
def read(container_path, path_name)
- 1
open(container_path) {|container|
- 1
container.read(path_name.to_s)
}
end
- 1
private
- 1
def _adapter
- 2
(self == PhysicalContainer) ? @adapter : self
end
end
- 1
def initialize(container_path)
- 7
@container_path = container_path
- 7
@monitor = Monitor.new
end
end
end
end
- 1
require 'archive/zip'
- 1
module EPUB
- 1
class OCF
- 1
class PhysicalContainer
- 1
class ArchiveZip < self
- 1
def initialize(container_path)
super
@entries = {}
@last_iterated_entry_index = 0
end
- 1
def open
Archive::Zip.open @container_path do |archive|
@monitor.synchronize do
@archive = archive
begin
yield self
ensure
@archive = nil
end
end
end
end
- 1
def read(path_name)
if @archive
target_index = @entries[path_name]
@archive.each.with_index do |entry, index|
if target_index
if target_index == index
return entry.file_data.read
else
next
end
end
next if index < @last_iterated_entry_index
# We can force encoding UTF-8 because EPUB spec allows only UTF-8 filenames
entry_path = entry.zip_path.force_encoding('UTF-8')
@entries[entry_path] = index
@last_iterated_entry_index = index
if entry_path == path_name
return entry.file_data.read
end
end
raise NoEntry, "Entry not found: #{path_name}"
else
open {|container| container.read(path_name)}
end
end
end
end
end
end
- 1
require "zip"
- 1
module EPUB
- 1
class OCF
- 1
class PhysicalContainer
- 1
class Rubyzip < self
- 1
def open
- 5
orig_encoding = Zip.force_entry_names_encoding
begin
- 5
Zip.force_entry_names_encoding = "UTF-8"
- 5
Zip::File.open @container_path do |archive|
- 5
@monitor.synchronize do
- 5
@archive = archive
begin
- 5
yield self
ensure
- 5
@archive = nil
end
end
end
ensure
- 5
Zip.force_entry_names_encoding = orig_encoding
end
end
- 1
def read(path_name)
- 6
if @archive
- 5
@archive.read(path_name)
else
- 2
open {|container| container.read(path_name)}
end
rescue Errno::ENOENT => error
raise NoEntry.from_error(error)
end
end
end
end
end
- 1
module EPUB
- 1
class OCF
- 1
class PhysicalContainer
- 1
class UnpackedDirectory < self
- 1
def open
yield self
end
- 1
def read(path_name)
::File.read(::File.join(@container_path, path_name))
rescue ::Errno::ENOENT => error
raise NoEntry.from_error(error)
end
end
end
end
end
- 1
require 'open-uri'
- 1
module EPUB
- 1
class OCF
- 1
class PhysicalContainer
- 1
class UnpackedURI < self
# EPUB URI: http://example.net/path/to/book/
# container.xml: http://example.net/path/to/book/META-INF/container.xml
# @param [URI, String] container_path URI of EPUB container's root directory.
# For exapmle, <code>"http://example.net/path/to/book/"</code>, which
# 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).
- 1
def initialize(container_path)
super(URI(container_path))
end
- 1
def open
yield self
end
- 1
def read(path_name)
(@container_path + path_name).read
rescue ::OpenURI::HTTPError => error
raise NoEntry.from_error(error)
end
end
end
end
end
- 1
module EPUB
- 1
class OCF
- 1
class Rights
end
end
end
- 1
module EPUB
- 1
class OCF
- 1
class Signatures
end
end
end
- 1
require 'epub'
- 1
require 'epub/constants'
- 1
require 'epub/book'
- 1
module EPUB
- 1
class Parser
- 1
class << self
# Parse an EPUB file
#
# @example
# EPUB::Parser.parse('path/to/book.epub') # => EPUB::Book object
#
# @example
# class MyBook
# include EPUB::Book::Feature
# end
# book = MyBook.new
# parsed_book = EPUB::Parser.parse('path/to/book.epub', book: book) # => #<MyBook:0x000000019760e8 @epub_file=..>
# parsed_book.equal? book # => true
#
# @example
# book = EPUB::Parser.parse('path/to/book.epub', class: MyBook) # => #<MyBook:0x000000019b0568 @epub_file=...>
# book.instance_of? MyBook # => true
#
# @param [String] filepath
# @param [Hash] options the type of return is specified by this argument.
# If no options, returns {EPUB::Book} object.
# For details of options, see below.
# @option options [EPUB] :book instance of class which includes {EPUB} module
# @option options [Class] :class class which includes {EPUB} module
# @option options [EPUB::OCF::PhysicalContainer, Symbol] :container_adapter OCF physical container adapter to use when parsing EPUB container
# When class passed, it is used. When symbol passed, it is considered as subclass name of {EPUB::OCF::PhysicalContainer}.
# If omitted, {EPUB::OCF::PhysicalContainer.adapter} is used.
# @return [EPUB] object which is an instance of class including {EPUB} module.
# When option :book passed, returns the same object whose attributes about EPUB are set.
# When option :class passed, returns the instance of the class.
# Otherwise returns {EPUB::Book} object.
- 1
def parse(filepath, container_adapter: nil, book: nil, initialize_with: nil, **options)
new(filepath, container_adapter: container_adapter, book: book, initialize_with: initialize_with, **options).parse
end
end
- 1
def initialize(filepath, container_adapter: nil, book: nil, initialize_with: nil, **options)
if filepath.to_s.encoding == Encoding::ASCII_8BIT
# On Windows and macOS, encoding of file name is set by Ruby,
# but on UNIX, always is ASCII-8BIT
# See https://docs.ruby-lang.org/ja/2.7.0/class/IO.html
filepath = filepath.to_s.dup
require "nkf"
filepath.force_encoding NKF.guess(filepath)
end
path_is_uri = (container_adapter == EPUB::OCF::PhysicalContainer::UnpackedURI or
container_adapter == :UnpackedURI or
EPUB::OCF::PhysicalContainer.adapter == EPUB::OCF::PhysicalContainer::UnpackedURI)
raise "File #{filepath} not found" if
!path_is_uri and !File.exist?(filepath)
@filepath = path_is_uri ? filepath : File.realpath(filepath)
@book = create_book(book: book, initialize_with: initialize_with, **options)
if path_is_uri
@book.container_adapter = :UnpackedURI
elsif File.directory? @filepath
@book.container_adapter = :UnpackedDirectory
end
@book.epub_file = @filepath
if options[:container_adapter]
@book.container_adapter = options[:container_adapter]
end
end
- 1
def parse
@book.container_adapter.open @filepath do |container|
@book.ocf = OCF.parse(container)
@book.ocf.container.rootfiles.each {|rootfile|
package = Publication.parse(container, rootfile.full_path.to_s)
rootfile.package = package
@book.packages << package
package.book = @book
}
end
@book
end
- 1
private
- 1
def create_book(book: nil, initialize_with: nil, **params)
case
when book
book
when params[:class]
if initialize_with
params[:class].new initialize_with
else
params[:class].new
end
else
Book.new
end
end
end
end
- 1
require 'epub/parser/version'
- 1
require 'epub/parser/xml_document'
- 1
require 'epub/parser/ocf'
- 1
require 'epub/parser/publication'
- 1
require 'epub/parser/content_document'
- 1
require 'epub/content_document'
- 1
require 'epub/constants'
- 1
require 'epub/parser/xml_document'
- 1
module EPUB
- 1
class Parser
- 1
class ContentDocument
- 1
using XMLDocument::Refinements
# @param [EPUB::Publication::Package::Manifest::Item] item
- 1
def initialize(item)
@item = item
end
- 1
def parse
content_document = case @item.media_type
when 'application/xhtml+xml'
if @item.nav?
EPUB::ContentDocument::Navigation.new
else
EPUB::ContentDocument::XHTML.new
end
when 'image/svg+xml'
EPUB::ContentDocument::SVG.new
else
nil
end
return content_document if content_document.nil?
content_document.item = @item
document = XMLDocument.new(@item.read)
# parse_content_document(document)
if @item.nav?
content_document.navigations = parse_navigations(document)
end
content_document
end
# @param [XMLDocument, REXML::Document, Oga::XML::Document, Nokogiri::HTML::Document] document HTML document or element including nav
# @return [Array<EPUB::ContentDocument::Navigation::Navigation>] navs array of Navigation object
- 1
def parse_navigations(document)
document.each_element_by_xpath('/xhtml:html/xhtml:body//xhtml:nav', EPUB::NAMESPACES).collect {|elem| parse_navigation elem}
end
# @param [REXML::Element, Oga::XML::Element, Nokogiri::XML::Element] element nav element
# @return [EPUB::ContentDocument::Navigation::Navigation] nav Navigation object
- 1
def parse_navigation(element)
nav = EPUB::ContentDocument::Navigation::Navigation.new
nav.text = find_heading(element)
hidden = element.attribute_with_prefix('hidden')
nav.hidden = hidden.nil? ? nil : true
nav.types = element.attribute_with_prefix('type', 'epub')&.split(/\s+/)
element.each_element_by_xpath('./xhtml:ol/xhtml:li', EPUB::NAMESPACES).map do |elem|
nav.items << parse_navigation_item(elem)
end
nav
end
# @param [REXML::Element, Oga::XML::Element, Nokogiri::XML::Element] element li element
- 1
def parse_navigation_item(element)
item = EPUB::ContentDocument::Navigation::Item.new
a_or_span = element.each_element_by_xpath('./xhtml:a[1]|xhtml:span[1]', EPUB::NAMESPACES).first
return a_or_span if a_or_span.nil?
item.text = a_or_span.content
item.types = a_or_span.attribute_with_prefix('type', 'epub')&.split(/\s+/)
if a_or_span.name == 'a'
if item.text.empty?
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
unless embedded_content.nil?
case embedded_content.name
when 'audio', 'canvas', 'embed', 'iframe'
item.text = embedded_content.attribute_with_prefix('name') || embedded_content.attribute_with_prefix('srcdoc')
when 'img'
item.text = embedded_content.attribute_with_prefix('alt')
when 'math', 'object'
item.text = embedded_content.attribute_with_prefix('name')
when 'svg', 'video'
else
end
end
item.text = a_or_span.attribute_with_prefix('title').to_s if item.text.nil? || item.text.empty?
end
item.href = a_or_span.attribute_with_prefix('href')
item.item = @item.find_item_by_relative_iri(item.href)
end
item.items = element.each_element_by_xpath('./xhtml:ol[1]/xhtml:li', EPUB::NAMESPACES).map {|li| parse_navigation_item(li)}
item
end
- 1
private
# @param [REXML::Element, Oga::XML::Element, Nokogiri::XML::Element] element nav element
# @return [String] heading heading text
- 1
def find_heading(element)
heading = element.each_element_by_xpath('./xhtml:h1|xhtml:h2|xhtml:h3|xhtml:h4|xhtml:h5|xhtml:h6|xhtml:hgroup', EPUB::NAMESPACES).first
return nil if heading.nil?
return heading.content unless heading.name == 'hgroup'
(heading.each_element_by_xpath(".//xhtml:h1", EPUB::NAMESPACES) ||
heading.each_element_by_xpath(".//xhtml:h2", EPUB::NAMESPACES) ||
heading.each_element_by_xpath(".//xhtml:h3", EPUB::NAMESPACES) ||
heading.each_element_by_xpath(".//xhtml:h4", EPUB::NAMESPACES) ||
heading.each_element_by_xpath(".//xhtml:h5", EPUB::NAMESPACES) ||
heading.each_element_by_xpath(".//xhtml:h6", EPUB::NAMESPACES)).first.content
end
end
end
end
- 1
module EPUB
- 1
class Parser
- 1
module Metadata
- 1
using XMLDocument::Refinements
- 1
def parse_metadata(elem, unique_identifier_id, default_namespace)
metadata = EPUB::Publication::Package::Metadata.new
id_map = {}
default_namespace_uri = EPUB::NAMESPACES[default_namespace]
elem.each_element do |child|
elem_name = child.name
model =
case child.namespace_uri
when EPUB::NAMESPACES['dc']
case elem_name
when 'identifier'
identifier = build_model(child, :Identifier, ['id'])
metadata.identifiers << identifier
identifier.scheme = child.attribute_with_prefix('scheme', 'opf')
identifier
when 'title'
title = build_model(child, :Title)
metadata.titles << title
title
when 'language'
language = build_model(child, :DCMES, ['id'])
metadata.languages << language
language
when 'contributor', 'coverage', 'creator', 'date', 'description', 'format', 'publisher', 'relation', 'source', 'subject', 'rights', 'type'
attr = elem_name == 'rights' ? elem_name : elem_name + 's'
dcmes = build_model(child)
metadata.__send__(attr) << dcmes
dcmes
else
build_unsupported_model(child)
end
when default_namespace_uri
case elem_name
when 'meta'
meta = build_model(child, :Meta, %w[property id scheme content name])
metadata.metas << meta
meta
when 'link'
link = build_model(child, :Link, %w[id media-type])
metadata.links << link
link.href = child.attribute_with_prefix('href')
link.rel = Set.new(child.attribute_with_prefix('rel').split(/\s+/))
link
else
build_unsupported_model(child)
end
else
build_unsupported_model(child)
end
metadata.children << model
if model.kind_of?(EPUB::Metadata::Identifier) &&
model.id == unique_identifier_id
metadata.unique_identifier = model
end
if model.respond_to?(:id) && model.id
id_map[model.id] = {refinee: model}
end
refines = child.attribute_with_prefix('refines')
if refines && refines.start_with?('#')
id = refines[1..-1]
id_map[id] ||= {}
id_map[id][:refiners] ||= []
id_map[id][:refiners] << model
end
end
id_map.values.each do |hsh|
next unless hsh[:refiners]
next unless hsh[:refinee]
hsh[:refiners].each {|meta| meta.refines = hsh[:refinee]}
end
metadata
end
- 1
def build_model(elem, klass=:DCMES, attributes=%w[id lang dir])
model = EPUB::Metadata.const_get(klass).new
attributes.each do |attr|
writer_name = (attr == "content") ? "meta_content=" : "#{attr.gsub('-', '_')}="
namespace = (attr == "lang") ? "xml" : nil
model.__send__ writer_name, elem.attribute_with_prefix(attr, namespace)
end
model.content = elem.content unless klass == :Link
model.content.strip! if klass == :Identifier
model
end
- 1
def build_unsupported_model(elem)
EPUB::Metadata::UnsupportedModel.new(elem)
end
end
end
end
- 1
require 'epub/constants'
- 1
require 'epub/ocf'
- 1
require 'epub/ocf/physical_container'
- 1
require 'epub/parser/metadata'
- 1
require "epub/parser/xml_document"
- 1
module EPUB
- 1
class Parser
- 1
class OCF
- 1
using XMLDocument::Refinements
- 1
include Metadata
- 1
DIRECTORY = 'META-INF'
- 1
class << self
- 1
def parse(container)
new(container).parse
end
end
- 1
def initialize(container)
@container = container
@ocf = EPUB::OCF.new
end
- 1
def parse
EPUB::OCF::MODULES.each do |m|
begin
data = @container.read(File.join(DIRECTORY, "#{m}.xml"))
@ocf.__send__ "#{m}=", __send__("parse_#{m}", data)
rescue EPUB::OCF::PhysicalContainer::NoEntry
end
end
@ocf
end
- 1
def parse_container(xml)
container = EPUB::OCF::Container.new
doc = XMLDocument.new(xml)
doc.each_element_by_xpath "/ocf:container/ocf:rootfiles/ocf:rootfile", EPUB::NAMESPACES do |elem|
rootfile = EPUB::OCF::Container::Rootfile.new
rootfile.full_path = Addressable::URI.parse(elem.attribute_with_prefix('full-path'))
rootfile.media_type = elem.attribute_with_prefix('media-type')
container.rootfiles << rootfile
end
container
end
- 1
def parse_encryption(content)
encryption = EPUB::OCF::Encryption.new
encryption.content = content
encryption
end
- 1
def parse_manifest(content)
warn "Not implemented: #{self.class}##{__method__}" if $VERBOSE
end
- 1
def parse_metadata(content)
doc = XMLDocument.new(content)
unless multiple_rendition_metadata?(doc)
warn "Not implemented: #{self.class}##{__method__}" if $VERBOSE
metadata = EPUB::OCF::UnknownFormatMetadata.new
metadata.content = content
return metadata
end
super(doc.root, doc.root.attribute_with_prefix('unique-identifier'), 'metadata')
end
- 1
def parse_rights(content)
warn "Not implemented: #{self.class}##{__method__}" if $VERBOSE
end
- 1
def parse_signatures(content)
warn "Not implemented: #{self.class}##{__method__}" if $VERBOSE
end
- 1
private
- 1
def multiple_rendition_metadata?(doc)
doc.root &&
doc.root.name == 'metadata' &&
doc.root.namespaces['xmlns'] == EPUB::NAMESPACES['metadata']
end
end
end
end
- 1
require 'strscan'
- 1
require 'epub/publication'
- 1
require 'epub/constants'
- 1
require 'epub/parser/metadata'
- 1
module EPUB
- 1
class Parser
- 1
class Publication
- 1
using XMLDocument::Refinements
- 1
include Metadata
- 1
class << self
- 1
def parse(container, file)
opf = container.read(Addressable::URI.unencode(file))
new(opf).parse
end
end
- 1
def initialize(opf)
@doc = XMLDocument.new(opf)
end
- 1
def parse
package = parse_package(@doc)
(EPUB::Publication::Package::CONTENT_MODELS - [:bindings]).each do |model|
package.__send__ "#{model}=", __send__("parse_#{model}", @doc)
end
package.bindings = parse_bindings(@doc, package.manifest)
package
end
- 1
def parse_package(doc)
package = EPUB::Publication::Package.new
elem = doc.root
%w[version xml:lang dir id].each do |attr|
package.__send__ "#{attr.gsub(/\:/, '_')}=", elem.attribute_with_prefix(attr)
end
package.prefix = parse_prefix(elem.attribute_with_prefix('prefix'))
EPUB::Publication.__send__ :include, EPUB::Publication::FixedLayout if package.prefix.key? EPUB::Publication::FixedLayout::PREFIX_KEY
package
end
- 1
def parse_metadata(doc)
super(doc.each_element_by_xpath('/opf:package/opf:metadata', EPUB::NAMESPACES).first, doc.root.attribute_with_prefix('unique-identifier'), 'opf')
end
- 1
def parse_manifest(doc)
manifest = EPUB::Publication::Package::Manifest.new
elem = doc.each_element_by_xpath('/opf:package/opf:manifest', EPUB::NAMESPACES).first
manifest.id = elem.attribute_with_prefix('id')
fallback_map = {}
elem.each_element_by_xpath('./opf:item', EPUB::NAMESPACES).each do |e|
item = EPUB::Publication::Package::Manifest::Item.new
%w[id media-type media-overlay].each do |attr|
item.__send__ "#{attr.gsub(/-/, '_')}=", e.attribute_with_prefix(attr)
end
item.href = e.attribute_with_prefix('href')
fallback = e.attribute_with_prefix('fallback')
fallback_map[fallback] = item if fallback
properties = e.attribute_with_prefix('properties')
item.properties = properties.split(' ') if properties
manifest << item
end
fallback_map.each_pair do |id, from|
from.fallback = manifest[id]
end
manifest
end
- 1
def parse_spine(doc)
spine = EPUB::Publication::Package::Spine.new
elem = doc.each_element_by_xpath('/opf:package/opf:spine', EPUB::NAMESPACES).first
%w[id toc page-progression-direction].each do |attr|
spine.__send__ "#{attr.gsub(/-/, '_')}=", elem.attribute_with_prefix(attr)
end
elem.each_element_by_xpath('./opf:itemref', EPUB::NAMESPACES).each do |e|
itemref = EPUB::Publication::Package::Spine::Itemref.new
%w[idref id].each do |attr|
itemref.__send__ "#{attr}=", e.attribute_with_prefix(attr)
end
itemref.linear = (e.attribute_with_prefix('linear') != 'no')
properties = e.attribute_with_prefix('properties')
itemref.properties = properties.split(' ') if properties
spine << itemref
end
spine
end
- 1
def parse_guide(doc)
guide = EPUB::Publication::Package::Guide.new
doc.each_element_by_xpath '/opf:package/opf:guide/opf:reference', EPUB::NAMESPACES do |ref|
reference = EPUB::Publication::Package::Guide::Reference.new
%w[type title].each do |attr|
reference.__send__ "#{attr}=", ref.attribute_with_prefix(attr)
end
reference.href = ref.attribute_with_prefix('href')
guide << reference
end
guide
end
- 1
def parse_bindings(doc, handler_map)
bindings = EPUB::Publication::Package::Bindings.new
doc.each_element_by_xpath '/opf:package/opf:bindings/opf:mediaType', EPUB::NAMESPACES do |elem|
media_type = EPUB::Publication::Package::Bindings::MediaType.new
media_type.media_type = elem.attribute_with_prefix('media-type')
media_type.handler = handler_map[elem.attribute_with_prefix('handler')]
bindings << media_type
end
bindings
end
- 1
def parse_prefix(str)
prefixes = {}
return prefixes if str.nil? or str.empty?
scanner = StringScanner.new(str)
scanner.scan /\s*/
while prefix = scanner.scan(/[^\:\s]+/)
scanner.scan /[\:\s]+/
iri = scanner.scan(/[^\s]+/)
if iri.nil? or iri.empty?
warn "no IRI detected for prefix `#{prefix}`"
else
prefixes[prefix] = iri
end
scanner.scan /\s*/
end
prefixes
end
end
end
end
- 1
module EPUB
- 1
class Parser
- 1
class XMLDocument
- 1
class << self
- 1
attr_accessor :backend
- 1
def new(xml)
case backend
when :Oga
Oga.parse_xml(xml)
when :Nokogiri
Nokogiri.XML(xml)
else
REXML::Document.new(xml)
end
end
end
end
end
end
- 1
%i[Nokogiri Oga REXML].each do |backend|
begin
- 3
require "epub/parser/xml_document/refinements/#{backend.downcase}"
- 3
EPUB::Parser::XMLDocument.backend ||= backend
rescue LoadError
end
end
- 1
require "nokogiri"
- 1
module EPUB
- 1
class Parser
- 1
class XMLDocument
- 1
module Refinements
- 1
module Nokogiri
- 1
refine ::Nokogiri::XML::Node do
- 1
def each_element_by_xpath(xpath, namespaces = nil, &block)
xpath(xpath, namespaces).each &block
end
- 1
def attribute_with_prefix(name, prefix = nil)
attribute_with_ns(name, EPUB::NAMESPACES[prefix])&.value
end
- 1
def each_element(xpath = nil, &block)
element_children.each(&block)
end
- 1
alias elements element_children
- 1
def namespace_uri
namespace.href
end
end
end
- 1
include Nokogiri
end
end
end
end
- 1
require "oga"
- 1
module EPUB
- 1
class Parser
- 1
class XMLDocument
- 1
module Refinements
- 1
module Oga
- 1
[::Oga::XML::Document, ::Oga::XML::Node].each do |klass|
- 2
refine klass do
[
- 2
[:document, ::Oga::XML::Document],
[:element, ::Oga::XML::Element],
[:text, ::Oga::XML::Text]
].each do |(type, klass)|
- 6
define_method "#{type}?" do
kind_of? klass
end
end
- 2
def root
# Couldn't use find(&:element?) for Rubies under 2.6
root_node.children.find {|child| child.element?}
end
- 2
def elements
# Couldn't use find(&:element?) for Rubies under 2.6
children.select {|child| child.element?}
end
# Need for Rubies under 2.6
- 2
def respond_to?(name, include_all = false)
[:root, :elements].include?(name) || super
end
- 2
def each_element_by_xpath(xpath, namespaces = nil, &block)
xpath(xpath, namespaces: namespaces).each &block
end
end
end
- 1
refine ::Oga::XML::Element do
- 1
def attribute_with_prefix(name, prefix = nil)
name = prefix ? "#{prefix}:#{name}" : name
get(name)
end
- 1
def each_element(xpath = nil, &block)
each_node do |node|
throw :skip_children unless node.kind_of?(::Oga::XML::Element)
block.call node
end
end
- 1
def namespace_uri
namespace&.uri
end
- 1
alias original_namespaces namespaces
- 1
def namespaces
original_namespaces.each_with_object({}) {|(name, namespace), nss|
nss[name] = namespace.uri
}
end
- 1
alias content text
end
- 1
refine ::Oga::XML::Text do
- 1
alias content text
end
end
- 1
include Oga
end
end
end
end
- 1
require "rexml/document"
- 1
module EPUB
- 1
class Parser
- 1
class XMLDocument
- 1
module Refinements
- 1
module REXML
- 1
[::REXML::Element, ::REXML::Text].each do |klass|
- 2
refine klass do
- 2
%i[document element text].each do |type|
- 6
define_method "#{type}?" do
node_type == type
end
end
end
end
- 1
refine ::REXML::Element do
- 1
def each_element_by_xpath(xpath, namespaces = nil, &block)
::REXML::XPath.each self, xpath, namespaces, &block
end
- 1
def attribute_with_prefix(name, prefix = nil)
attribute(name, EPUB::NAMESPACES[prefix])&.value
end
- 1
alias namespace_uri namespace
- 1
def content
each_child.inject("") {|text, node|
case node.node_type
when :document, :element
text << node.content
when :text
text << node.value
end
}
end
end
- 1
refine ::REXML::Text do
- 1
alias content value
end
end
- 1
include REXML
end
end
end
end
- 1
require 'epub/publication/package'
- 1
require 'epub/publication/fixed_layout'
- 1
module EPUB
- 1
module Publication
- 1
module FixedLayout
- 1
PREFIX_KEY = 'rendition'.freeze
- 1
PREFIX_VALUE = 'http://www.idpf.org/vocab/rendition/#'.freeze
RENDITION_PROPERTIES = {
- 1
'layout' => ['reflowable'.freeze, 'pre-paginated'.freeze].freeze,
'orientation' => ['auto'.freeze, 'landscape'.freeze, 'portrait'.freeze].freeze,
'spread' => ['auto'.freeze, 'none'.freeze, 'landscape'.freeze, 'portrait'.freeze, 'both'.freeze].freeze
}.freeze
- 1
class UnsupportedRenditionValue < StandardError; end
- 1
class << self
- 1
def included(package_class)
[
[Package, PackageMixin],
[Package::Metadata, MetadataMixin],
[Package::Spine::Itemref, ItemrefMixin],
[Package::Manifest::Item, ItemMixin],
[ContentDocument::XHTML, ContentDocumentMixin],
].each do |(base, mixin)|
base.__send__ :include, mixin
end
end
end
- 1
module Rendition
# @note Call after defining #rendition_xxx and #renditionn_xxx=
- 1
def def_rendition_methods
- 4
RENDITION_PROPERTIES.each_key do |property|
- 12
alias_method property, "rendition_#{property}"
- 12
alias_method "#{property}=", "rendition_#{property}="
end
- 4
def_rendition_layout_methods
end
- 1
def def_rendition_layout_methods
- 4
property = 'layout'
- 4
RENDITION_PROPERTIES[property].each do |value|
- 8
method_name_base = value.gsub('-', '_')
- 8
writer_name = "#{method_name_base}="
- 8
define_method writer_name do |new_value|
new_prop = new_value ? value : values.find {|l| l != value}
__send__ "rendition_#{property}=", new_prop
end
- 8
maker_name = "make_#{method_name_base}"
- 8
define_method maker_name do
__send__ "rendition_#{property}=", value
end
- 8
destructive_method_name = "#{method_name_base}!"
- 8
alias_method destructive_method_name, maker_name
- 8
predicate_name = "#{method_name_base}?"
- 8
define_method predicate_name do
__send__("rendition_#{property}") == value
end
end
end
end
- 1
module PackageMixin
# @return [true, false]
- 1
def using_fixed_layout
prefix.has_key? PREFIX_KEY and
prefix[PREFIX_KEY] == PREFIX_VALUE
end
- 1
alias using_fixed_layout? using_fixed_layout
# @param using_fixed_layout [true, false]
- 1
def using_fixed_layout=(using_fixed_layout)
if using_fixed_layout
prefix[PREFIX_KEY] = PREFIX_VALUE
else
prefix.delete PREFIX_KEY
end
end
end
- 1
module MetadataMixin
- 1
extend Rendition
- 1
RENDITION_PROPERTIES.each_pair do |property, values|
- 3
define_method "rendition_#{property}" do
meta = metas.find {|m| m.property == "rendition:#{property}"}
meta ? meta.content : values.first
end
- 3
define_method "rendition_#{property}=" do |new_value|
raise UnsupportedRenditionValue, new_value unless values.include? new_value
prefixed_property = "rendition:#{property}"
values_to_be_deleted = values - [new_value]
metas.delete_if {|meta| meta.property == prefixed_property && values_to_be_deleted.include?(meta.content)}
unless metas.any? {|meta| meta.property == prefixed_property && meta.content == new_value}
meta = Package::Metadata::Meta.new
meta.property = prefixed_property
meta.content = new_value
metas << meta
end
new_value
end
end
- 1
def_rendition_methods
end
- 1
module ItemrefMixin
- 1
extend Rendition
- 1
PAGE_SPREAD_PROPERTY = 'center'
- 1
PAGE_SPREAD_PREFIX = 'rendition:page-spread-'
- 1
class << self
# @todo Define using Module#prepend after Ruby 2.0 will become popular
- 1
def included(base)
return if base.instance_methods.include? :page_spread_without_fixed_layout
base.__send__ :alias_method, :page_spread_without_fixed_layout, :page_spread
base.__send__ :alias_method, :page_spread_writer_without_fixed_layout, :page_spread=
prefixed_page_spread_property = "#{PAGE_SPREAD_PREFIX}#{PAGE_SPREAD_PROPERTY}"
base.__send__ :define_method, :page_spread do
property = page_spread_without_fixed_layout
return property if property
properties.include?(prefixed_page_spread_property) ? PAGE_SPREAD_PROPERTY : nil
end
base.__send__ :define_method, :page_spread= do |new_value|
if new_value == PAGE_SPREAD_PROPERTY
page_spread_writer_without_fixed_layout nil
properties << prefixed_page_spread_property
else
page_spread_writer_without_fixed_layout new_value
end
new_value
end
end
end
- 1
RENDITION_PROPERTIES.each do |property, values|
- 3
rendition_property_prefix = "rendition:#{property}-"
- 3
reader_name = "rendition_#{property}"
- 3
define_method reader_name do
prop_value = properties.find {|prop| prop.start_with? rendition_property_prefix}
prop_value ? prop_value.gsub(/\A#{Regexp.escape(rendition_property_prefix)}/, '') :
spine.package.metadata.__send__(reader_name)
end
- 3
writer_name = "#{reader_name}="
- 3
define_method writer_name do |new_value|
if new_value.nil?
properties.delete_if {|prop| prop.start_with? rendition_property_prefix}
return new_value
end
raise UnsupportedRenditionValue, new_value unless values.include? new_value
values_to_be_deleted = (values - [new_value]).map {|value| "#{rendition_property_prefix}#{value}"}
properties.delete_if {|prop| values_to_be_deleted.include? prop}
new_property = "#{rendition_property_prefix}#{new_value}"
properties << new_property unless properties.include? new_property
new_value
end
end
- 1
def_rendition_methods
end
- 1
module ItemMixin
- 1
extend Rendition
- 1
RENDITION_PROPERTIES.each_key do |property|
- 3
define_method "rendition_#{property}" do
itemref.__send__ property
end
- 3
writer_name = "rendition_#{property}="
- 3
define_method writer_name do |value|
itemref.__send__ writer_name, value
end
end
- 1
def_rendition_methods
end
- 1
module ContentDocumentMixin
- 1
extend Rendition
- 1
RENDITION_PROPERTIES.each_key do |property|
- 3
reader_name = "rendition_#{property}"
- 3
define_method reader_name do
item.__send__ reader_name
end
- 3
writer_name = "rendition_#{property}="
- 3
define_method writer_name do |value|
item.__send__ writer_name, value
end
end
- 1
def_rendition_methods
end
end
end
end
- 1
module EPUB
- 1
module Publication
- 1
class Package
- 1
include Inspector
- 1
CONTENT_MODELS = [:metadata, :manifest, :spine, :guide, :bindings]
RESERVED_VOCABULARY_PREFIXES = {
- 1
'' => 'http://idpf.org/epub/vocab/package/#',
'dcterms' => 'http://purl.org/dc/terms/',
'marc' => 'http://id.loc.gov/vocabulary/',
'media' => 'http://www.idpf.org/epub/vocab/overlays/#',
'onix' => 'http://www.editeur.org/ONIX/book/codelists/current.html#',
'xsd' => 'http://www.w3.org/2001/XMLSchema#'
}
- 1
class << self
- 1
def define_content_model(model_name)
- 5
define_method "#{model_name}=" do |model|
current_model = __send__(model_name)
current_model.package = nil if current_model
model.package = self
instance_variable_set "@#{model_name}", model
end
end
end
- 1
attr_accessor :book,
:version, :prefix, :xml_lang, :dir, :id
- 1
attr_reader *CONTENT_MODELS
- 1
alias lang xml_lang
- 1
alias lang= xml_lang=
- 1
CONTENT_MODELS.each do |model|
- 5
define_content_model model
end
- 1
def initialize
@prefix = {}
end
# @return [EPUB::Metadata::Identifier] Unique Identifier
- 1
def unique_identifier
@metadata.unique_identifier
end
# Corresponding {Rootfile}
# @return [OCF::Container::Rootfile]
- 1
def rootfile
@book.ocf.container.rootfiles.find {|rf| rf.package == self}
end
# Full path in EPUB archive
# @return [Addressable::URI]
- 1
def full_path
rootfile.full_path if rootfile
end
- 1
def inspect
"#<%{class}:%{object_id} %{attributes} %{models}>" % {
:class => self.class,
:object_id => inspect_object_id,
:attributes => inspect_instance_variables(exclude: CONTENT_MODELS.map {|model| :"@#{model}"}),
:models => inspect_models
}
end
- 1
def inspect_models
CONTENT_MODELS.map {|name|
model = __send__(name)
representation = model.nil? ? model.inspect : model.inspect_simply
"@#{name}=#{representation}"
}.join(' ')
end
end
end
end
- 1
EPUB::Publication::Package::CONTENT_MODELS.each do |f|
- 5
require_relative "package/#{f}"
end
- 1
module EPUB
- 1
module Publication
- 1
class Package
- 1
class Bindings
- 1
include Inspector::PublicationModel
- 1
attr_accessor :package
- 1
def initialize
@media_types = {}
end
- 1
def <<(media_type)
@media_types[media_type.media_type] = media_type
end
- 1
def [](media_type)
_, mt = @media_types.detect {|key, _| key == media_type}
mt
end
- 1
def media_types
@media_types.values
end
- 1
class MediaType
- 1
attr_accessor :media_type, :handler
end
end
end
end
end
- 1
module EPUB
- 1
module Publication
- 1
class Package
- 1
class Guide
- 1
include Inspector::PublicationModel
- 1
attr_accessor :package, :references
- 1
def initialize
Reference::TYPES.each do |type|
variable_name = '@' + type.gsub('-', '_')
instance_variable_set variable_name, nil
end
@references = []
end
- 1
def <<(reference)
reference.guide = self
references << reference
end
- 1
class Reference
- 1
TYPES = %w[cover title-page toc index glossary acknowledgements bibliography colophon copyright-page dedication epigraph foreword loi lot notes preface text]
- 1
attr_accessor :guide,
:type, :title
- 1
attr_reader :href
- 1
def href=(iri)
@href = iri.kind_of?(Addressable::URI) ? iri : Addressable::URI.parse(iri)
end
- 1
def item
return @item if @item
request_uri = href.request_uri
@item = @guide.package.manifest.items.find {|item|
item.href.request_uri == request_uri
}
end
end
- 1
Reference::TYPES.each do |type|
- 17
method_name = type.gsub('-', '_')
- 17
define_method method_name do
var = instance_variable_get "@#{method_name}"
return var if var
var = references.find {|ref| ref.type == type}
instance_variable_set "@#{method_name}", var
end
end
end
end
end
end
- 1
require 'set'
- 1
require 'addressable/uri'
- 1
require 'rchardet'
- 1
require 'epub/constants'
- 1
require 'epub/parser/content_document'
- 1
module EPUB
- 1
module Publication
- 1
class Package
- 1
class Manifest
- 1
include Inspector::PublicationModel
- 1
attr_accessor :package,
:id
- 1
def initialize
@items = {}
end
# @param item [Item]
# @return [Manifest] self
- 1
def <<(item)
item.manifest = self
@items[item.id] = item
self
end
- 1
def each_nav
if block_given?
each_item do |item|
yield item if item.nav?
end
else
each_item.lazy.select(&:nav?)
end
end
# @return [Array<Item>] item which includes "nav" as one of +properties+. It represents this item is a navigation of book.
- 1
def navs
items.select(&:nav?)
end
# @return [Item, nil] the first item of #navs
- 1
def nav
navs.first
end
# @return [Item, nil] item which includes "cover-image" as one of +properties+. It represents this item is cover image.
- 1
def cover_image
items.find(&:cover_image?)
end
# @overload each_item
# @yield [item]
# @yieldparam [Item]
# @overload each_item
# @return [Enumerator]
- 1
def each_item
if block_given?
@items.each_value do |item|
yield item
end
else
@items.each_value
end
end
- 1
def items
@items.values
end
# @param item_id [String]
# @return [Item, nil]
- 1
def [](item_id)
@items[item_id]
end
- 1
class Item
- 1
DUMMY_ROOT_IRI = Addressable::URI.parse('http://example.net/').freeze
- 1
include Inspector
# @!attribute [rw] manifest
# @return [Manifest] Returns the value of manifest
# @!attribute [rw] id
# @return [String] Returns the value of id
# @!attribute [rw] href
# @return [Addressable::URI] Returns the value of href,
# which is relative IRI from rootfile(OPF file)
# @!attribute [rw] media_type
# @return [String] Returns the value of media_type
# @!attribute [rw] properties
# @return [Set<String>] Returns the value of properties
# @!attribute [rw] media_overlay
# @return [String] Returns the value of media_overlay
# @!attribute [rw] fallback
# @return [Item] Returns the value of attribute fallback
- 1
attr_accessor :manifest,
:id, :media_type, :fallback, :media_overlay
- 1
attr_reader :properties, :href
- 1
def initialize
@properties = Set.new
@full_path = nil
end
- 1
def properties=(props)
@properties = props.kind_of?(Set) ? props : Set.new(props)
end
- 1
def href=(iri)
@href = iri.kind_of?(Addressable::URI) ? iri : Addressable::URI.parse(iri)
end
# @todo Handle circular fallback chain
- 1
def fallback_chain
@fallback_chain ||= traverse_fallback_chain([])
end
# full path in archive
# @return [Addressable::URI]
- 1
def full_path
return @full_path if @full_path
path = DUMMY_ROOT_IRI + manifest.package.full_path + href
path.scheme = nil
path.host = nil
path.path = path.path[1..-1]
@full_path = path
end
# full path in archive
# @return [String]
- 1
def entry_name
Addressable::URI.unencode(full_path)
end
# Read content from EPUB archive
#
# @param detect_encoding [Boolean] Whether #read tries auto-detection of character encoding. The default value is +false+.
# @return [String] Content with encoding:
# US-ASCII when the content is not in text format such images.
# UTF-8 when the content is in text format and +detect_encoding+ is +false+.
# auto-detected encoding when the content is in text format and +detect_encoding+ is +true+.
- 1
def read(detect_encoding: false)
raw_content = manifest.package.book.container_adapter.read(manifest.package.book.epub_file, entry_name)
unless media_type.start_with?('text/') or
media_type.end_with?('xml') or
['application/json', 'application/javascript', 'application/ecmascript', 'application/xml-dtd'].include?(media_type)
return raw_content
end
if detect_encoding
# CharDet.detect doesn't raise Encoding::CompatibilityError
# that is caused when trying compare CharDet's internal
# ASCII-8BIT RegExp with a String with other encoding
# because Zip::File#read returns a String with encoding ASCII-8BIT.
# So, no need to rescue the error here.
encoding = CharDet.detect(raw_content)['encoding']
if encoding
raw_content.force_encoding(encoding)
else
warn "No encoding detected for #{entry_name}. Set to ASCII-8BIT" if $DEBUG || $VERBOSE
raw_content
end
else
raw_content.force_encoding("UTF-8");
end
end
- 1
def xhtml?
media_type == 'application/xhtml+xml'
end
- 1
def nav?
properties.include? 'nav'
end
- 1
def cover_image?
properties.include? 'cover-image'
end
# @todo Handle circular fallback chain
- 1
def use_fallback_chain(options = {})
supported = EPUB::MediaType::CORE
if ad = options[:supported]
supported = supported | (ad.respond_to?(:to_ary) ? ad : [ad])
end
if del = options[:unsupported]
supported = supported - (del.respond_to?(:to_ary) ? del : [del])
end
return yield self if supported.include? media_type
if (bindings = manifest.package.bindings) && (binding_media_type = bindings[media_type])
return yield binding_media_type.handler
end
return fallback.use_fallback_chain(options) {|fb| yield fb} if fallback
raise EPUB::MediaType::UnsupportedMediaType
end
- 1
def content_document
return nil unless %w[application/xhtml+xml image/svg+xml].include? media_type
@content_document ||= Parser::ContentDocument.new(self).parse
end
# @return [Package::Spine::Itemref]
# @return nil when no Itemref refers this Item
- 1
def itemref
manifest.package.spine.itemrefs.find {|itemref| itemref.idref == id}
end
# @param iri [Addressable::URI] relative iri
# @return [Item]
# @return [nil] when item not found
# @raise ArgumentError when +iri+ is not relative
# @raise ArgumentError when +iri+ starts with "/"(slash)
# @note Algorithm stolen form Rack::Utils#clean_path_info
- 1
def find_item_by_relative_iri(iri)
raise ArgumentError, "Not relative: #{iri.inspect}" unless iri.relative?
raise ArgumentError, "Start with slash: #{iri.inspect}" if iri.path.start_with? Addressable::URI::SLASH
target_href = href + iri
target_href.fragment = nil
segments = target_href.to_s.split(Addressable::URI::SLASH)
clean_segments = []
segments.each do |segment|
next if segment.empty? || segment == '.'
segment == '..' ? clean_segments.pop : clean_segments << segment
end
target_iri = Addressable::URI.parse(clean_segments.join(Addressable::URI::SLASH))
manifest.items.find { |item| item.href == target_iri}
end
- 1
def inspect
"#<%{class}:%{object_id} %{manifest} %{attributes}>" % {
:class => self.class,
:object_id => inspect_object_id,
:manifest => "@manifest=#{@manifest.inspect_simply}",
:attributes => inspect_instance_variables(exclude: [:@manifest])
}
end
- 1
protected
- 1
def traverse_fallback_chain(chain)
chain << self
return chain unless fallback
fallback.traverse_fallback_chain(chain)
end
end
end
end
end
end
- 1
require 'epub/metadata'
- 1
module EPUB
- 1
module Publication
- 1
class Package
- 1
Metadata = EPUB::Metadata
end
end
end
- 1
require 'set'
- 1
module EPUB
- 1
module Publication
- 1
class Package
- 1
class Spine
- 1
include Inspector::PublicationModel
- 1
attr_accessor :package,
:id, :toc, :page_progression_direction
- 1
attr_reader :itemrefs
- 1
def initialize
@itemrefs = []
end
# @return self
- 1
def <<(itemref)
itemref.spine = self
@itemrefs << itemref
self
end
# @yield [itemref]
# @yieldparam [Itemref] itemref
# @yieldreturn [Object] returns the last value of block
# @return [Object, Enumerator]
# returns the last value of block when block given, Enumerator when not
- 1
def each_itemref
if block_given?
itemrefs.each {|itemref| yield itemref}
else
enum_for :each_itemref
end
end
# @return [Enumerator] Enumerator which yeilds {Manifest::Item}
# referred by each of {#itemrefs}
- 1
def items
itemrefs.collect {|itemref| itemref.item}
end
- 1
class Itemref
- 1
PAGE_SPREAD_PROPERTIES = ['left'.freeze, 'right'.freeze].freeze
- 1
PAGE_SPREAD_PREFIX = 'page-spread-'.freeze
- 1
attr_accessor :spine,
:idref, :linear, :id
- 1
attr_reader :properties
- 1
def initialize
@properties = Set.new
end
- 1
def properties=(props)
@properties = props.kind_of?(Set) ? props : Set.new(props)
end
# @return [true|false]
- 1
def linear?
!! linear
end
# @return [Package::Manifest::Item] item referred by this object
- 1
def item
@item ||= @spine.package.manifest[idref]
end
- 1
def item=(item)
self.idref = item.id
item
end
- 1
def ==(other)
[:spine, :idref, :id].all? {|meth|
self.__send__(meth) == other.__send__(meth)
} and
(linear? == other.linear?) and
(properties == other.properties)
end
# @return ["left", "right", nil]
- 1
def page_spread
property = properties.find {|prop| prop.start_with? PAGE_SPREAD_PREFIX}
property ? property.gsub(/\A#{Regexp.escape(PAGE_SPREAD_PREFIX)}/, '') : nil
end
# @param new_value ["left", "right", nil]
- 1
def page_spread=(new_value)
if new_value.nil?
properties.delete_if {|prop| prop.start_with? PAGE_SPREAD_PREFIX}
return new_value
end
raise "Unsupported page-spread property: #{new_value}" unless PAGE_SPREAD_PROPERTIES.include? new_value
props_to_be_deleted = (PAGE_SPREAD_PROPERTIES - [new_value]).map {|prop| "#{PAGE_SPREAD_PREFIX}#{prop}"}
properties.delete_if {|prop| props_to_be_deleted.include? prop}
new_property = "#{PAGE_SPREAD_PREFIX}#{new_value}"
properties << new_property unless properties.include? new_property
new_value
end
end
end
end
end
end