lib/gepub/item.rb

Summary

Maintainability
B
4 hrs
Test Coverage
A
98%
module GEPUB
  #
  # an Object to hold metadata and content of item in manifest.
  #
  # following methods are created dynamically.
  # #id, #id=, #set_id, #href, #href=, #set_href, #media_type, #media_type=, #set_media_type,
  # #fallback, #fallback=, #set_fallback, #media_overlay, #media_overlay=, #set_media_overlay
  class Item
    include InspectMixin

    attr_accessor :content
    def self.create(parent, attributes = {})
      Item.new(attributes['id'], attributes['href'], attributes['media-type'], parent,
               attributes.reject { |k,_v| ['id','href','media-type'].member?(k) })
    end

    #
    # create Item.
    # 
    # if mediatype is not specified, it will be guessed from extension name.
    # Item can't guess media type for videos and  audios, so you should specify one.
    # 
    def initialize(itemid, itemhref, itemmediatype = nil, parent = nil, attributes = {})
      if attributes['properties'].class == String
        attributes['properties'] = attributes['properties'].split(' ')
      end
      @attributes = {'id' => itemid, 'href' => itemhref, 'media-type' => itemmediatype}.merge(attributes)
      @attributes['media-type'] = GEPUB::Mime.guess_mediatype(itemhref) if media_type.nil?
      @parent = parent
      @parent.register_item(self) unless @parent.nil?
      self
    end

    ATTRIBUTES = ['id', 'href', 'media-type', 'fallback', 'properties', 'media-overlay'].each { |name|
      methodbase = name.sub('-','_')
      define_method(methodbase + '=') { |val| @attributes[name] = val }
      define_method('set_' + methodbase) { |val| @attributes[name] = val; self }
      define_method(methodbase) { @attributes[name] }
    }

    # get item's id
    def itemid
      id
    end

    # get mediatype of the item.
    def mediatype
      media_type
    end

    # get +attribute+
    def [](attribute)
      @attributes[attribute]
    end

    # set +attribute+
    def []=(attribute, value)
      @attributes[attribute] = value
    end

    # add value to properties attribute.
    def add_property(property)
      (@attributes['properties'] ||=[]) << property
      self
    end

    # set 'cover-image' property to the Item.
    # On generating EPUB, EPUB2-style cover image meta item will be added.
    def cover_image
      add_property('cover-image')
    end

    # set 'nav' property to the Item.
    def nav
      add_property('nav')
    end

    # set toc text to the item
    def toc_text text
      toc.push(:item => self, :text => text, :id => nil)
      self
    end

    # set toc text with id to the item
    def toc_text_with_id text, toc_id
      toc.push(:item => self, :text => text, :id => toc_id)
      self
    end

    # set bindings: item is a handler for media_type
    def is_handler_of media_type
      bindings.add(self.id, media_type)
      self
    end

    def landmark(type:, title:, id: nil)
      landmarks.push(:type => type, :title => title, :item => self, :id => id)
      self
    end

    # guess and set content property from contents.
    def guess_content_property
      if File.extname(self.href) =~ /.x?html/ && @attributes['media-type'] === 'application/xhtml+xml'
        @attributes['properties'] ||= []
        parsed = Nokogiri::XML::Document.parse(@content)
        return unless parsed.root.node_name === "html"
        ns_prefix =  parsed.namespaces.invert['http://www.w3.org/1999/xhtml']
        if ns_prefix.nil?
          prefix = ''
        else
          prefix = "#{ns_prefix}:"
        end
        images = parsed.xpath("//#{prefix}img[starts-with(@src,'http')]")
        videos = parsed.xpath("//#{prefix}video[starts-with(@src,'http')]") + parsed.xpath("//#{prefix}video/#{prefix}source[starts-with(@src,'http')]")
        audios = parsed.xpath("//#{prefix}audio[starts-with(@src,'http')]") + parsed.xpath("//#{prefix}audio/#{prefix}source[starts-with(@src,'http')]")
        if images.size > 0 || videos.size > 0 || audios.size > 0
          self.add_property('remote-resources')
        end
        if parsed.xpath("//p:math", { 'p' => 'http://www.w3.org/1998/Math/MathML' }).size > 0
          self.add_property('mathml')
        end
        if parsed.xpath("//s:svg", { 's' => 'http://www.w3.org/2000/svg' }).size > 0
          self.add_property('svg')
        end
        if parsed.xpath("//epub:switch", { 'epub' => 'http://www.idpf.org/2007/ops' }).size > 0
          self.add_property('switch')
        end
        scripts = parsed.xpath("//#{prefix}script") + parsed.xpath("//#{prefix}form")
        if scripts.size > 0
          self.add_property('scripted')
        end
      end
    end

    # add content data to the item.
    def add_raw_content(data)
      @content = data
      if File.extname(self.href) =~ /x?html$/
        @content.force_encoding('utf-8')
      end
      guess_content_property
      self
    end

    # add content from io or file to the item
    def add_content(io_or_filename)
      if io_or_filename.class == String
        File.open(io_or_filename, mode='r') do |f|
          add_content_io f
        end
      else
        add_content_io io_or_filename
      end
      self
    end

    def add_content_io(io)
      io.binmode
      @content = io.read
      if File.extname(self.href) =~ /x?html$/
        @content.force_encoding('utf-8')
      end
      guess_content_property
      self
    end

    # generate xml to supplied Nokogiri builder.
    def to_xml(builder, opf_version = '3.0')
      attr = @attributes.dup
      if opf_version.to_f < 3.0
        attr.reject!{ |k,_v| k == 'properties' }
      end
      if !attr['properties'].nil?
        attr['properties'] = attr['properties'].uniq.join(' ')
        if attr['properties'].size == 0
          attr.delete 'properties'
        end
      end
      builder.item(attr)
    end
  end
end