FrontPage  Index  Search  Changes  Atom  PageRank  Login

Hiki Atomプラグイン

Atomプラグイン

 Hikiの最近の更新をAtomフィードで配信するプラグインです。

 v0.8(2010年10月25日)から、ページ毎にAtom Entryも配信できるようになりました。もしHikiをAtomPub対応にしようという心意気のある方がいれば手間を省くのにどうぞ。

 Rackブランチではなく、オリジナルのHiki用です(バージョン0.8.8.1で動作確認済み)。

 Rackブランチ版のプラグインはRackPluginAtomをご覧ください。このHikidashiでもこれを使っています。

ダウンロード

インストール

 上のファイルをダウンロードして展開すると

  • atom.rb
  • en/atom.rb
  • ja/atom.rb

の三ファイルが出てきます。 それぞれ

  • misc/plugin
  • misc/plugin/en
  • misc/plugin/ja

ディレクトリーに入れてください。

 プラグインを有効化すれば自動でAtomフィードが配信されます。

 配信する内容(全文かダイジェストか、どんなフォーマットか)、Atom Entryを配信するかどうか、「Atom」「Atom Entry」という文字をページ内にも表示するかどうか、Atom Feedで何件の記事を配信するかは、管理画面から選択できます。

ソース

atom.rb

# = $Id: atom.rb, v1.0 2011-01-31
# Copyright (C) 2011 KITAITI Makoto <KitaitiMakoto@gmail.com>
#
# A Hiki plugin to syndicate Atom feed 
#
# rss.rb plugin is very helpful for this plugin, thanks!
# 
# == Requirements
#  * rss library which can make Atom feeds
#    (If your rss lib cannot, download from 
#     http://www.cozmixng.org/~rwiki/?cmd=view;name=RSS+Parser)
# 
# == Environment
#  Operation checked under:
#  * Ruby 1.8.7
#  * Hiki 0.8.8.1
# 
# == To Do
#  * Refactoring(Extract Method)
require 'time'

def atom
  if @conf['atom.entry.enable'] && @page
    atom_entry(@page)
  else
    atom_feed
  end
  
  nil # Don't move to the 'FrontPage'
end

def atom_feed
  page_count = @conf['atom.count'] || atom_default_page_count
  pages = atom_recent_updates(page_count)
  last_modified = pages.first.values[0][:last_modified]
  header = {}
  
  if_modified_since = ENV['HTTP_IF_MODIFIED_SINCE']
  if_modified_since = Time.parse(if_modified_since) if if_modified_since
  
  if if_modified_since and last_modified <= if_modified_since
    header['status'] = 'NOT_MODIFIED'
    print @cgi.header(header)
  else
    body = atom_body(pages)
    header['Last-Modified'] = last_modified.httpdate
    header['type']  = 'application/atom+xml'
    header['charset']       =  body.encoding
    header['Content-Language'] = @conf.lang
    header['Pragma']           = 'no-cache'
    header['Cache-Control']    = 'no-cache'
    print @cgi.header(header)
    print euc_to_utf8(body.to_s)
  end
end

def atom_entry(page_name)
  page = @db.info(page_name)
  return print @cgi.header({'status' => 'NOT_FOUND'}) unless page
  return print @cgi.header({'status' => 'FORBIDDEN'}) if respond_to?(:viewable?) && !viewable?(page.keys[0].to_s) # for private-view.rb plugin
  
  last_modified = page[:last_modified]
  header = {}
  
  if_modified_since = ENV['HTTP_IF_MODIFIED_SINCE']
  if_modified_since = Time.parse(if_modified_since) if if_modified_since
  
  if if_modified_since and last_modified <= if_modified_since
    header['status'] = 'NOT_MODIFIED'
    print @cgi.header(header)
  else
    require 'rss/maker'
    
    body = RSS::Maker.make('atom:entry') do |maker|
      atom_make_entry(maker, {page_name => page }, :default_author => 'anonymous', :mode => :html_full)
    end
    
    header['Last-Modified'] = last_modified.httpdate
    header['type']  = 'application/atom+xml'
    header['charset']       = body.encoding
    header['Content-Language'] = @conf.lang
    header['Pragma']           = 'no-cache'
    header['Cache-Control']    = 'no-cache'
    print @cgi.header(header)
    print euc_to_utf8(body.to_s)
  end
end

def atom_recent_updates(page_count = atom_default_page_count)
  pages = @db.page_info
  pages.reject! {|page| private_view_private_page?(page.keys[0])} if respond_to? :private_view_private_page? # for private-view.rb plugin
  pages.sort_by do |p|
    p[p.keys[0]][:last_modified]
  end.last(page_count).reverse
end

def atom_body(pages)
  require 'rss/maker'
  
  RSS::Maker.make('atom') do |maker|
    maker.encoding = 'UTF-8'
    
    maker.channel.author = @conf.author_name
    maker.channel.about = @conf.index_url + '?c=atom'
    maker.channel.title = @conf.site_name + ' : ' + label_atom_recent
    maker.channel.description = @conf.site_name + ' ' + label_atom_recent
    maker.channel.language = @conf.lang
    maker.channel.date = pages.first.values[0][:last_modified]
    maker.channel.rights = 'Copyright (C) ' + @conf.author_name
    maker.channel.generator do |generator|
      generator.uri = 'http://hikiwiki.org/'
      generator.version = ::Hiki::VERSION
      generator.content = 'Hiki'
    end
    maker.channel.links.new_link do |link|
      link.rel = 'self'
      link.type = 'application/atom+xml'
      link.href = maker.channel.about
    end
    maker.channel.links.new_link do |link|
      link.rel = 'alternate'
      link.type = 'text/html'
      link.href = @conf.index_url + '?c=recent'
    end
    
    pages.each do |page|
      atom_make_entry(maker, page)
    end
  end
end

def atom_make_entry(maker, page, options = {})
  maker.items.new_item do |item|
    name = page.keys[0]    
    uri = @conf.index_url + '?' + name.escape
    
    item.title = page_name(name)
    item.link = uri
    item.author = options[:default_author] if options[:default_author]
    item.author = page[name][:editor] if page[name][:editor]
    item.date = page[name][:last_modified].utc.strftime('%Y-%m-%dT%H:%M:%S+00:00')
    item.content.type = 'html'
    item.content.content = atom_make_content(page, options[:mode])
  end
end

def atom_make_content(page, mode = nil)
  mode ||= @conf['atom.mode']
  raise ArgumentError, "Unknown mode: #{mode}" unless [:unidiff, :worddiff_digest, :worddiff_full, :html_full].include?(mode)
  
  name = page.keys[0]
  src = @db.load_backup(name) || ''
  dst = @db.load(name) || ''
  
  case mode
  when :unidiff
    content = h(unified_diff(src, dst)).strip.gsub(/\n/, "<br>\n").gsub(/ /, '&nbsp;')
  when :worddiff_digest
    content = word_diff(src, dst, true).strip.gsub(/\n/, "<br>\n")
  when :worddiff_full
    content = word_diff(src, dst).strip.gsub(/\n/, "<br>\n")
  when :html_full
    tokens = @db.load_cache(name)
    unless tokens
      parser = @conf.parser.new(@conf)
      tokens = parser.parse(@db.load(name))
      @db.save_cache(name, tokens)
    end
    tmp = @conf.use_plugin
    begin
      @conf.use_plugin = false
      formatter = @conf.formatter.new(tokens, @db, Plugin.new(@conf.options, @conf), @conf)
      content = formatter.to_s
    ensure
      @conf.use_plugin = tmp
    end
  end
  if content.empty?
    content = shorten(dst).strip.gsub(/\n/, "<br>\n")
  end
  
  content
end

def atom_max_page_count; 50; end
def atom_default_page_count; 10; end

add_body_enter_proc do
  @conf['atom.mode'] ||= :unidiff
  @conf['atom.menu'] ? add_plugin_command('atom', 'Atom') : add_plugin_command('atom', nil)
end

add_header_proc do
  %Q|  <link rel="alternate" type="application/atom+xml" title="Atom" href="#{@conf.index_url}?c=atom">\n|
end

if @conf['atom.entry.enable']
  if @conf['atom.entry.menu-display']
    add_menu_proc {%Q|<a href="#{@conf.index_url}?c=atom;p=#{@page.escape}">Atom Entry</a>|} if @page
  end
  add_header_proc { %Q|  <link rel="alternate" type="application/atom+xml" title="Atom Entry" href="#{@conf.index_url}?c=atom;p=#{@page.escape}">| } if @page
end

def atom_saveconf
  if @mode == 'saveconf'
    @conf['atom.mode'] = @cgi.params['atom.mode'][0].intern
    @conf['atom.count'] = [@cgi.params['atom.count'][0].to_i, atom_max_page_count].min
    @conf['atom.count'] = [@conf['atom.count'], 1].max
  end 
end

if @cgi.params['conf'][0] == 'atom' && @mode == 'saveconf'
  @conf['atom.menu'] = (@cgi.params['atom.menu'][0] == 'true')
  @conf['atom.entry.enable'] = (@cgi.params['atom.entry.enable'][0] == 'true')
  @conf['atom.entry.menu-display'] = (@cgi.params['atom.entry.menu-display'][0] == 'true')
end

add_conf_proc('atom', label_atom_config) do
  atom_saveconf
  
  str = <<HTML
  <h3 class="subtitle">#{label_atom_mode_title}</h3>
  <p><select name="atom.mode">
HTML
  label_atom_mode_candidate.each_pair do |value, label|
    str << %Q|<option value="#{value}"#{' selected' if @conf['atom.mode'] == value}>#{label}</option>\n|
  end
  str << "</select></p>\n"
  
  str << <<HTML
  <h3 class="subtitle">#{label_atom_menu_title}</h3>
  <p><label><input type="radio" name="atom.menu" value="true"
                   #{'checked="checked"' if @conf['atom.menu']}>
            #{label_atom_menu_enable}</label>
     <label><input type="radio" name="atom.menu" value="false"
                   #{'checked="checked"' unless @conf['atom.menu']}>
            #{label_atom_menu_disable}</label>\n
HTML
  
  str << <<HTML
  <h3 class="subtitle">#{label_atom_count_title}</h3>
  <p><input name="atom.count" size="4"
      value="#{@conf['atom.count']}">
     #{label_atom_count_unit}(#{label_atom_max_page_count})</p>
HTML
  
  str << <<HTML
  <h3 class="subtitle">#{label_atom_entry_enable_title}</h3>
  <p><label><input type="radio" name="atom.entry.enable" value="true"
             #{'checked="checked"' if @conf['atom.entry.enable']}>
             #{label_atom_entry_enable}</label>
     <label><input type="radio" name="atom.entry.enable" value="false"
             #{'checked="checked"' unless @conf['atom.entry.enable']}>
             #{label_atom_entry_disable}</label></p>
  <h3 class="subtitle">#{label_atom_entry_menu_display_title}</h3>
  <p><label><input type="radio" name="atom.entry.menu-display" value="true"
            #{'checked="checked"' if @conf['atom.entry.menu-display']}>
            #{label_atom_entry_menu_display}</label>
     <label><input type="radio" name="atom.entry.menu-display" value="false"
            #{'checked="checked"' unless @conf['atom.entry.menu-display']}>
            #{label_atom_entry_menu_hide}</label></p>
HTML
end

export_plugin_methods :atom

en/atom.rb

# en/atom.rb

def label_atom_recent
  'Recent Changes'
end

def label_atom_config; 'Atom syndication'; end
def label_atom_mode_title; 'Select the format of the feed.' end
def label_atom_mode_candidate
  {
    :unidiff => 'unified diff',
    :worddiff_digest => 'word diff(digest)',
    :worddiff_full => 'word diff(full text)',
    :html_full => 'HTML(full text)',
  }
end
def label_atom_menu_title; 'Add Atom Feed menu'; end
def label_atom_menu_candidate
  [ 'Yes', 'No' ]
end
def label_atom_count_title; 'The count of syndicated feeds'; end
def label_atom_count_unit; 'pages'; end
def label_atom_max_page_count; "#{atom_max_page_count} at a max"; end

def label_atom_entry_enable_title; 'Syndicate Atom Entry per page'; end
def label_atom_entry_enable; 'Yes'; end
def label_atom_entry_disable; 'No'; end
def label_atom_entry_menu_display_title; 'Add Atom Entry menu'; end
def label_atom_entry_menu_display; 'Yes'; end
def label_atom_entry_menu_hide; 'No'; end

ja/atom.rb

# -*- coding: euc-jp -*-
# ja/atom.rb

def label_atom_recent
  '更新日時順'
end

def label_atom_config; 'Atom の配信'; end
def label_atom_mode_title; 'Atom Feedのフォーマット'; end
def label_atom_mode_candidate
  {
    :unidiff => 'unified diff 形式',
    :worddiff_digest => 'word diff 形式 (ダイジェスト)',
    :worddiff_full => 'word diff 形式 (全文)',
    :html_full => 'HTML 形式 (全文)',
  }
end
def label_atom_menu_title; 'Atom Feedメニューの表示'; end
def label_atom_menu_candidate
  [ 'する', 'しない' ]
end
def label_atom_count_title; 'Atom Feedで配信するページの数'; end
def label_atom_count_unit; '件'; end
def label_atom_max_page_count; "最大#{atom_max_page_count}件"; end

def label_atom_entry_enable_title; '各ページのAtom Entryの配信'; end
def label_atom_entry_enable; 'する'; end
def label_atom_entry_disable; 'しない'; end
def label_atom_entry_menu_display_title; '各ページのAtom Entryメニューの表示'; end
def label_atom_entry_menu_display; 'する'; end
def label_atom_entry_menu_hide; 'しない'; end

メモ

  • エントリーごとにキャッシュを作って、古いページのキャッシュと新しいページから作ったエントリーとを組み合わせてフィードを構築するとちょっとパフォーマンスがいいかも知れない。
    • Ruby標準添付のrssライブラリーを使っているが、特定のエントリーだけ構築済みのXML要素を使うってできるんだろうか。
    • そう言えば、例えばrss-showプラグインなんかを使って動的にページを作るやつのキャッシュは考えなきゃいけない、よくある問題だけど。Ajaxで読み込んじゃうのがいいと思うんだよな。
  • Atom Entry配信の際、毎回info.dbにアクセスしなければならないのが気に食わないが、HTTPのIf-Modified-Since付きのリクエストに対応する為には仕方が無い。大域幅を節約するかサーバー負荷を減らすかっていう問題だが。
    • キャッシュファイルを作るんだったら、それを読み込んで解析してしまえばそのページのLast-Modifiedは出るから、info.dbに触らないでもIf-Modified-Sinceに対応できるかも知れない。その場合は勿論Atom Entryを解析する負荷がサーバーに掛かるが、とても壊れやすいinfo.dbに触るよりはましなんじゃないかという気がする。それに、Last-Modifiedとentry要素のペアでキャッシュしてもいいしね。
  • キーワード毎の最新記事を配信できるようにしたい。
Last modified:2011/05/22 12:15:29
Keyword(s):
References:[自作プラグイン]
Referer | 282 | 280 | 113 | 38 | 15 | 13 | 13 | 10 | 10 | 9 | 7 | 7 | 6 | 6 | 6 | 6 | 5 | 5 | 5 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 3 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |