lib/hatenablog/entry.rb
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