lib/gepub/package.rb
require 'rubygems'
require 'nokogiri'
require 'forwardable'
module GEPUB
# Holds data in opf file.
class Package
include XMLUtil, DSLUtil
include InspectMixin
extend Forwardable
attr_accessor :path, :metadata, :manifest, :spine, :bindings, :epub_backward_compat, :contents_prefix, :prefixes
def_delegators :@manifest, :item_by_href
def_delegators :@metadata, *Metadata::CONTENT_NODE_LIST.map {
|x|
if x == "identifier"
["#{x}_list", "set_#{x}", "add_#{x}"]
else
["#{x}", "#{x}_list", "set_#{x}", "#{x}=", "add_#{x}"]
end
}.flatten
def_delegators :@metadata, :set_lastmodified
def_delegators :@metadata, :lastmodified
def_delegators :@metadata, :lastmodified=
def_delegators :@metadata, :modified_now
def_delegators :@metadata, :rendition_layout
def_delegators :@metadata, :rendition_layout=
def_delegators :@metadata, :rendition_orientation
def_delegators :@metadata, :rendition_orientation=
def_delegators :@metadata, :rendition_spread
def_delegators :@metadata, :rendition_spread=
def_delegators :@metadata, :ibooks_version
def_delegators :@metadata, :ibooks_version=
def_delegators :@metadata, :ibooks_scroll_axis
def_delegators :@metadata, :ibooks_scroll_axis=
def_delegators :@spine, :page_progression_direction=
def_delegators :@spine, :page_progression_direction
class IDPool
def initialize
@pool = {}
@counter = {}
end
def counter(prefix,suffix)
@counter[prefix + '////' + suffix]
end
def set_counter(prefix,suffix,val)
@counter[prefix + '////' + suffix] = val
end
def generate_key(param = {})
prefix = param[:prefix] || ''
suffix = param[:suffix] || ''
count = [ param[:start] || 1, counter(prefix,suffix) || 1].max
while (true)
if param[:without_count]
k = prefix + suffix
count -= 1
param.delete(:without_count)
else
k = prefix + count.to_s + suffix
end
if @pool[k].nil?
set_counter(prefix,suffix, count + 1)
return k
end
count += 1
end
end
def [](k)
@pool[k]
end
def []=(k,v)
@pool[k] = v
end
end
def parse_prefixes(prefix)
return {} if prefix.nil?
m = prefix.scan(/([\S]+): +(\S+)[\s]*/)
Hash[*m.flatten]
end
# parse OPF data. opf should be io or string object.
def self.parse_opf(opf, path)
Package.new(path) {
|package|
package.instance_eval {
@path = path
@xml = Nokogiri::XML::Document.parse(opf)
@namespaces = @xml.root.namespaces
@attributes = attr_to_hash(@xml.root.attributes)
@metadata = Metadata.parse(@xml.at_xpath("//#{ns_prefix(OPF_NS)}:metadata"), @attributes['version'], @id_pool)
@manifest = Manifest.parse(@xml.at_xpath("//#{ns_prefix(OPF_NS)}:manifest"), @attributes['version'], @id_pool)
@spine = Spine.parse(@xml.at_xpath("//#{ns_prefix(OPF_NS)}:spine"), @attributes['version'], @id_pool)
@bindings = Bindings.parse(@xml.at_xpath("//#{ns_prefix(OPF_NS)}:bindings"))
@prefixes = parse_prefixes(@attributes['prefix'])
}
}
end
def initialize(path='OEBPS/package.opf', attributes={})
@path = path
if File.extname(@path) != '.opf'
if @path.size > 0
@path = [path,'package.opf'].join('/')
end
end
@contents_prefix = File.dirname(@path).sub(/^\.$/,'')
@contents_prefix = @contents_prefix + '/' if @contents_prefix.size > 0
@prefixes = {}
@namespaces = {'xmlns' => OPF_NS }
@attributes = attributes
@attributes['version'] ||= '3.0'
@id_pool = IDPool.new
@metadata = Metadata.new(version)
@manifest = Manifest.new(version)
@spine = Spine.new(version)
@bindings = Bindings.new
@epub_backward_compat = true
yield self if block_given?
end
['unique-identifier', 'xml:lang', 'dir', 'prefix', 'id'].each {
|name|
methodbase = name.gsub('-','_').sub('xml:lang', 'lang')
define_method(methodbase + '=') { |val| @attributes[name] = val }
define_method('set_' + methodbase) { |val|
warn "set_#{methodbase} is obsolete. use #{methodbase} instead."
@attributes[name] = val
}
define_method(methodbase, ->(val=UNASSIGNED) {
if unassigned?(val)
@attributes[name]
else
send(methodbase + '=', val)
end
})
}
# get +attribute+
def [](attribute)
@attributes[attribute]
end
# set +attribute+
def []=(attribute, value)
@attributes[attribute] = value
end
def identifier(identifier=UNASSIGNED)
if unassigned?(identifier)
@metadata.identifier_by_id(unique_identifier)
else
self.identifier=(identifier)
end
end
def identifier=(identifier)
primary_identifier(identifier, nil, nil)
end
def primary_identifier(identifier, id = nil, type = nil)
unique_identifier(id || @id_pool.generate_key(:prefix => 'BookId', :without_count => true))
@metadata.add_identifier identifier, unique_identifier, type
end
def add_item(href, content:nil, id: nil, attributes: {})
item = @manifest.add_item(id, href, nil, attributes)
item.add_content(content) unless content.nil?
@spine.push(item) if @ordered
yield item if block_given?
item
end
def ordered
raise 'need block.' if !block_given?
@ordered = true
yield
@ordered = nil
end
def add_ordered_item(href, content:nil, id: nil, attributes: {})
raise 'do not call add_ordered_item within ordered block.' if @ordered
item = add_item(href, attributes: attributes, id:id, content: content)
@spine.push(item)
item
end
def spine_items
spine.itemref_list.map {
|itemref|
@manifest.item_list[itemref.idref]
}
end
def items
@manifest.item_list
end
def version(val=UNASSIGNED)
if unassigned?(val)
@attributes['version']
else
@attributes['version'] = val
@metadata.opf_version = val
@manifest.opf_version = val
@spine.opf_version = val
end
end
def set_version(val)
warn 'set_version is obsolete: use verion instead.'
@attributes['version'] = val
@metadata.opf_version = val
@manifest.opf_version = val
@spine.opf_version = val
end
def version=(val)
version(val)
end
def enable_rendition
@prefixes['rendition'] = 'http://www.idpf.org/vocab/rendition/#'
end
def rendition_enabled?
@prefixes['rendition'] == 'http://www.idpf.org/vocab/rendition/#'
end
def enable_ibooks_vocabulary
@prefixes['ibooks'] = 'http://vocabulary.itunes.apple.com/rdf/ibooks/vocabulary-extensions-1.0/'
end
def ibooks_vocabulary_enabled?
@prefixes['ibooks'] == 'http://vocabulary.itunes.apple.com/rdf/ibooks/vocabulary-extensions-1.0/'
end
def opf_xml
if version.to_f < 3.0 || @epub_backward_compat
spine.toc ||= 'ncx'
if @metadata.oldstyle_meta.select {
|meta|
meta['name'] == 'cover'
}.length == 0
@manifest.item_list.each {
|_k, item|
if item.properties && item.properties.member?('cover-image')
@metadata.add_oldstyle_meta(nil, 'name' => 'cover', 'content' => item.id)
end
}
end
end
if @metadata.rendition_specified? || @spine.rendition_specified?
enable_rendition
end
if @metadata.ibooks_vocaburaly_specified?
enable_ibooks_vocabulary
end
builder = Nokogiri::XML::Builder.new {
|xml|
if @prefixes.size == 0
@attributes.delete 'prefix'
else
@attributes['prefix'] = @prefixes.map { |k, v| "#{k}: #{v}" }.join(' ')
end
xml.package(@namespaces.merge(@attributes)) {
@metadata.to_xml(xml)
@manifest.to_xml(xml)
@spine.to_xml(xml)
@bindings.to_xml(xml)
}
}
builder.to_xml(:encoding => 'utf-8')
end
end
end