lib/gepub/package.rb

Summary

Maintainability
B
6 hrs
Test Coverage
A
94%
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