kymmt90/hatenablog

View on GitHub
lib/hatenablog/entry.rb

Summary

Maintainability
A
2 hrs
Test Coverage
A
99%
require 'nokogiri'
require 'time'

module Hatenablog
  module AfterHook
    # @dynamic uri=, edit_uri=, author_name=, title=, content=, updated=, draft=, categories=
    # @dynamic instance_methods, alias_method, define_method

    # Register a hooking method for given methods.
    # The hook method is executed after calling given methods.
    # @param [Symbol] hooking method name
    # @param [Array] hooked methods name array
    def after_hook(hook, *methods)
      methods.each do |method|
        origin_method = "#{method}_origin".to_sym
        if instance_methods.include? origin_method
          raise NameError, "#{origin_method} isn't a unique name"
        end

        alias_method origin_method, method

        define_method(method) do |*args, &block|
          # @type var block: ^(*untyped) -> untyped
          result = send(origin_method, *args, &block)
          send(hook)
          result
        end
      end
    end
  end

  class Entry
    extend AfterHook

    # @dynamic uri, uri=, author_name, author_name=, title, title=, content, content=, draft, draft=
    attr_accessor :uri, :author_name, :title, :content, :draft

    # @dynamic edit_uri, id, updated
    attr_reader :edit_uri, :id, :updated

    # @dynamic categories=
    attr_writer :categories

    def updated=(date)
      @updated = Time.parse(date)
    end

    def edit_uri=(uri)
      @edit_uri = uri
      @id       = uri.split('/').last
    end

    after_hook :update_xml, :uri=, :edit_uri=, :author_name=, :title=, :content=, :updated=, :draft=, :categories=

    # Create a new blog entry from a XML string.
    # @param [String] xml XML string representation
    # @return [Hatenablog::Entry]
    def self.load_xml(xml)
      Hatenablog::Entry.new(xml)
    end

    # Create a new blog entry from arguments.
    # @param [String] uri entry URI
    # @param [String] edit_uri entry URI for editing
    # @param [String] author_name entry author name
    # @param [String] title entry title
    # @param [String] content entry content
    # @param [String] draft this entry is draft if 'yes', otherwise it is not draft
    # @param [Array] categories categories array
    # @param [String] updated entry updated datetime (ISO 8601)
    # @return [Hatenablog::Entry]
    def self.create(uri: '', edit_uri: '', author_name: '', title: '',
                    content: '', draft: 'no', categories: [], updated: '')
      entry = Hatenablog::Entry.new(self.build_xml(uri, edit_uri, author_name, title,
                                                   content, draft, categories, updated))
      yield entry if block_given?
      entry
    end

    # @return [Boolean]
    def draft?
      @draft == 'yes'
    end

    # @return [Array]
    def categories
      @categories.dup
    end

    def each_category
      @categories.each do |category|
        yield category
      end
    end

    # @return [String]
    def to_xml
      @document.to_s.gsub(/\"/, "'")
    end

    # @return [String]
    def formatted_content
      @formatted_content
    end

    def self.build_xml(uri, edit_uri, author_name, title, content, draft, categories, updated)
      builder = Nokogiri::XML::Builder.new(encoding: 'utf-8') do |xml|
        xml.entry('xmlns'     => 'http://www.w3.org/2005/Atom',
                  'xmlns:app' => 'http://www.w3.org/2007/app') do
          xml.link(href: edit_uri, rel: 'edit')
          xml.link(href: uri,      rel: 'alternate', type: 'text/html')
          xml.author do
            xml.name author_name
          end
          xml.title title
          xml.content(content, type: 'text/x-markdown')
          xml.updated updated unless updated.nil? || updated.empty?
          unless categories.nil?
            categories.each do |category|
              xml.category(term: category)
            end
          end
          xml['app'].control do
            xml['app'].draft draft
          end
        end
      end

      builder.to_xml
    end


    private

    def initialize(xml)
      @document = Nokogiri::XML(xml)
      parse_document
    end

    def parse_document
      @uri         = @document.at_css('link[@rel="alternate"]')['href'].to_s
      @edit_uri    = @document.at_css('link[@rel="edit"]')['href'].to_s
      @id          = @edit_uri.split('/').last
      @author_name = @document.at_css('author name').content
      @title       = @document.at_css('title').content
      @content     = @document.at_css('content').content
      @formatted_content = @document.xpath('//hatena:formatted-content', hatena: 'http://www.hatena.ne.jp/info/xmlns#')[0]
      @formatted_content = @formatted_content.content if @formatted_content
      @draft       = @document.at_css('entry app|control app|draft').content
      @categories  = parse_categories
      if @document.at_css('entry updated')
        @updated = Time.parse(@document.at_css('entry updated').content)
      else
        @updated = nil
      end
    end

    def parse_categories
      categories = @document.css('category').inject(Array.new) do |categories, category|
        categories << category['term'].to_s
      end
      categories
    end

    def update_xml
      @document.at_css('author name').content                 = @author_name
      @document.at_css('title').content                       = @title
      @document.at_css('link[@rel="alternate"]')['href']      = @uri
      @document.at_css('link[@rel="edit"]')['href']           = @edit_uri
      @document.at_css('content').content                     = @content
      @document.at_css('entry app|control app|draft').content = @draft

      unless @updated.nil? || @document.at_css('entry updated').nil?
        @document.at_css('entry updated').content = @updated&.iso8601
      end

      unless @categories.nil?
        old_categories = @document.css('category')
        return if old_categories.empty? || !categories_modified?(old_categories, @categories)

        prev_node = @document.at_css('category').previous
        old_categories.each do |category|
          category.remove
        end

        @categories.each do |category|
          prev_node.next = @document.create_element('category', term: category)
          prev_node = prev_node.next
        end
      end
    end

    def categories_modified?(old_categories, new_categories)
      old_set = Set.new(old_categories.map { |category| category['term'] })
      new_set = Set.new(new_categories)
      old_set != new_set
    end
  end
end