Opera プラグイン Obook の目次を HTML にする

Obook は設定されたディレクトリのルートに XML ファイル (obook.xml) を置いて、そのディレクトリ以下に収めたスクラップした Web サイトデータを管理しているようだ。

他のブラウザからもそれまで溜めたスクラップを見れるように、その XML ファイルをごにょごにょして HTML にする。

Nokogiri で SAX が思ったより気軽だった。

require 'nokogiri'
require 'stringio'
require 'optparse'


class OBook
  def initialize
    @root = Folder.new
    @version = nil
  end
  
  attr_reader :root
  attr_reader :version
  
  def compile(visitor)
    visitor.apply_document{ traverse_for_visitor(visitor, root) }
  end
  
  def traverse_for_visitor(visitor, folder)
    folder.each do |ent|
      if ent.page?
        visitor.apply_page(ent)
      else
        visitor.apply_folder(ent){ traverse_for_visitor(visitor, ent) }
      end
    end
  end
  
  class Entry
    def initialize(table = {})
      table.each do |key, val|
        instance_variable_set "@#{key.downcase}", val
      end
    end
    
    attr_reader :title
    attr_reader :date
    
    def page?
      not folder?
    end
  end
  
  class Folder < Entry
    def entries
      @entries ||= []
    end
    
    def folder?
      true
    end
    
    def <<(ent)
      entries << ent
    end
    
    def each(&block)
      entries.each(&block)
    end
  end
  
  class Page < Entry
    attr_reader :url
    
    def folder?
      false
    end
  end
  
  class Document < Nokogiri::XML::SAX::Document
    def initialize(obook)
      @obook = obook
      @stack = [obook.root]
    end
    
    def start_element(name, attrs = [])
      case name
      when 'OBOOKS' then @obook.instance_eval{ @version = Hash[*attrs]['VERSION'] }
      when 'FOLDER' then @stack.push Folder.new(Hash[*attrs])
      when 'PAGE'   then @stack.last << Page.new(Hash[*attrs])
      end
    end
    
    def end_element(name)
      case name
      when 'FOLDER'
        folder = @stack.pop
        @stack.last << folder
      end
    end
  end
  
  class HTMLVisitor
    def initialize(root_dir)
      @root_dir = root_dir
      @f = StringIO.new
      @indent_cache = {}
    end
    
    def string
      @f.string
    end
    
    def indent
      @indent_cache[@nest] ||= '  ' * @nest
    end
    
    def nesting(tag = nil)
      puts "<#{tag}>" if tag
      @nest += 1
      ret = yield()
      @nest -= 1
      puts "</#{tag}>" if tag
      ret
    end
    
    def puts(str)
      @f.puts "#{indent}#{str}"
    end
    
    def apply_document(&block)
      @nest = 0
      nesting 'html' do
        nesting 'head' do
          puts "<title>OBook Index</title>"
          puts %Q`<style type="text/css">`
          puts "<!--"
          nesting do
            (<<-CSS).lines.each{|line| puts line.lstrip }
            body{ font-size : 90%; }
            h1{
              border-left   : solid 20px;
              border-bottom : solid 2px;
              padding-left  : 0.5em;
            }
            a{ 	color : #000000; }
            CSS
          end
          puts "-->"
          puts "</style>"
        end
        nesting 'body' do
          puts "<h1>OBook Index</h1>"
          nesting 'ul', &block
        end
      end
    end
    
    def apply_folder(folder, &block)
      nesting 'li' do
        puts folder.title
        nesting 'ul', &block
      end
    end
    
    def apply_page(page)
      puts %Q`<li><a href="file:///#{File.join(@root_dir, page.date, 'index.htm')}" title="#{page.date} - #{page.url}">#{page.title}</a></li>`
    end
  end
end


def with_output(filename, alt, &block)
  filename ? File.open(filename, 'w', &block) : yield(alt)
end

input  = nil
output = nil

ARGV.push '-o', 'obook.html'

o = OptionParser.new
o.on '-f', '--input=XML', 'specify OBook XML file' do |filename|
  input = filename
end
o.on '-o', '--output=DEST', 'specify output file' do |filename|
  output = filename
end

input ||= 'obook.xml'

begin
  o.parse! ARGV
  obook   = OBook.new
  visitor = OBook::HTMLVisitor.new(File.dirname(File.expand_path(input)))
  parser  = Nokogiri::XML::SAX::Parser.new(OBook::Document.new(obook))
  File.open(input){|f| parser.parse_io(f) }
  obook.compile(visitor)
  with_output(output, $stdout){|f| f.puts visitor.string }
rescue => ex
  $stderr.puts "#{File.basename($0, '.rb')}: #{ex.message}"
  exit 1
end