Hatena::Grouprubyist

Rubyで遊ぶよ

 | 

2011-04-02

はてな記法で ePub を作ってみた

17:04

そういう需要があったので作ってみた。一応メモとして残しておく。

その README から抜粋。(hatena2epub.bat というのは hatena2epub.rb にカレントディレクトリを引数として渡すだけのバッチファイル)

Hatena2ePub

はてな記法で書かれた複数のテキストファイルをまとめて ePub にします。

使い方

  1. hatena2epub.bat と同じフォルダ内に TXT というフォルダを作ります。
  2. その中に、はてな記法のテキストを .txt という拡張子で保存します。
  3. hatena2epub.bat をダブルクリックして実行します。
  4. DOS ウィンドウが立ち上がり、成功すれば BOOK.epub というファイルが出来ます。

その他

もし hatena2epub.bat と同じフォルダ内に、以下のようなファイルがあれば、それを設定として適用します。

ファイル名 説明 デフォルト設定
STYLE.css ePubCSS 空の CSS
TITLE.txt 本のタイトル "The Book"
AUTHOR.txt 本の著者 "Me"
LANG.txt 本で主に使われる言語 "ja"

BOOK.epub 作成時に BOOK というフォルダが出来ますが、これは消しても構いません。BOOK.epub は単に BOOK フォルダの中身を zip したものです。

次に hatena2epub.bat を実行したときには BOOK フォルダの中身は削除されます。

目次には各テキストの(拡張子部分を除いた)ファイル名、または、ファイルの1行目が見出しである場合はその見出しが使われます。

hatena2epub.rb はこうなってる。

#!/usr/bin/env ruby

require 'zip/zipfilesystem'
require 'hparser'
require 'hparser/text' # .to_text method
require 'uuid'
require 'kconv'
require 'erb'
require 'pathname'

class String
  def to_h
    # html escape
    self.gsub(/[<>"&]/) {|c|
      case c
      when '<'
        '&lt;'
      when '>'
        '&gt;'
      when '"'
        '&quot'
      when '&'
        '&amp;'
      end
    }
  end

  def /(other)
    File.join(self, other)
  end
end

class Hatena2ePub
  TEXT_DIR = "TXT"
  TEXT_FILES = "*.txt"
  TITLE_FILE = "TITLE.txt"
  DEFAULT_TITLE = "The Book"
  AUTHOR_FILE = "AUTHOR.txt"
  DEFAULT_AUTHOR = "Me"
  LANG_FILE = "LANG.txt"
  DEFAULT_LANG = "ja"
  ZIP_FILE = "BOOK.epub"
  BOOK_DIR = "BOOK"
  CONTENTS_DIR = "contents"
  MIMETYPE_FILE = "mimetype"
  MIMETYPE_CONTENT = "application/epub+zip"
  CONTAINER_XML_FILE = "META-INF" / "container.xml"
  CONTAINER_XML_TEMPLATE = <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<container xmlns="urn:oasis:names:tc:opendocument:xmlns:container" version="1.0">
  <rootfiles>
    <rootfile full-path="<%= OPF_FILE.to_h %>" media-type="application/oebps-package+xml" />
  </rootfiles>
</container>
EOF
  OPF_FILE = "metadata.opf"
  OPF_TEMPLATE = <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<package version="2.0" xmlns="http://www.idpf.org/2007/opf" unique-identifier="BookId">
 <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
   <dc:title><%= @title.to_h %></dc:title>
   <dc:creator opf:role="aut"><%= @author.to_h %></dc:creator>
   <dc:language><%= @language.to_h %></dc:language>
   <dc:identifier id="BookId"><%= @unique_id.to_h %></dc:identifier>
 </metadata>
 <manifest>
  <item id="ncx" href="<%= NCX_FILE.to_h %>" media-type="text/xml" />
  <item id="style" href="<%= CSS_FILE.to_h %>" media-type="text/css" />
<%= opf_html_items %>
 </manifest>
 <spine toc="ncx">
<%= opf_idrefs %>
 </spine>
</package>
EOF
  OPF_ITEM_XHTML_TEMPLATE = <<EOF
  <item id="<%= id.to_h %>" href="<%= file.to_h %>" media-type="application/xhtml+xml" />
EOF
  OPF_IDREF_TEMPLATE = <<EOF
  <itemref idref="<%= id.to_h %>" />
EOF
  NCX_FILE = "toc.ncx"
  NCX_TEMPLATE = <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1">
  <head>
    <meta name="dtb:uid" content="<%= @unique_id.to_h %>"/>
    <meta name="dtb:depth" content="1"/>
    <meta name="dtb:totalPageCount" content="0"/>
    <meta name="dtb:maxPageNumber" content="0"/>
  </head>
  <docTitle>
    <text><%= @title.to_h %></text>
  </docTitle>
  <docAuthor>
    <text><%= @author.to_h %></text>
  </docAuthor>
  <navMap>
<%= ncx_nav_points %>
  </navMap>
</ncx>
EOF
  NCX_NAV_POINT_TEMPLATE = <<EOF
    <navPoint id="<%= id.to_h %>" playOrder="1">
      <navLabel>
        <text><%= title.to_h %></text>
      </navLabel>
      <content src="<%= file.to_h %>"/>
    </navPoint>
EOF
  CSS_FILE = "STYLE.css"
  CSS_CONTENT = <<EOF
/* CSS to be applied to the ePub book */
EOF
  XHTML_TEMPLATE = <<EOF
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xlink="http://www.w3.org/1999/xlink" lang="<%= @language %>" xml:lang="<%= @language %>">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title><%= title.to_h %></title>
    <link href="<%= CSS_FILE.to_h %>" type="text/css" charset="UTF-8" rel="stylesheet"/>
  </head>
  <body>
<%= xhtml_body %>
  </body>
</html>
EOF

  def initialize(args)
    parse_opt(args)
    @file_list = []
    @zip = nil
    @uuid_obj = UUID.new
    @unique_id = @uuid_obj.generate
    @hparser = HParser::Parser.new
  end

  def parse_opt(args)
    if args.length < 1
      raise "usage: hatena2epub.rb working_dir"
    else
      @working_dir = args[0]
      @working_dir = @working_dir.gsub(File::ALT_SEPARATOR, File::SEPARATOR) unless File::ALT_SEPARATOR.nil?
      puts "working directory is #{@working_dir}"
    end
  end

  def start
    clear_existing
    get_config
    get_file_list
    convert_hatena
    make_epub
    puts "#{ZIP_FILE} was created."
  end

  def clear_existing
    if File.exist?(@working_dir / ZIP_FILE)
      if File.file?(@working_dir / ZIP_FILE)
        Pathname(@working_dir / ZIP_FILE).unlink 
      else
        raise "#{@working_dir / ZIP_FILE} is not a file"
      end
    end
    if File.exist?(@working_dir / BOOK_DIR)
      if File.directory?(@working_dir / BOOK_DIR)
        Pathname(@working_dir / BOOK_DIR).rmtree
      else
        raise "#{@working_dir / BOOK_DIR} is not a directory"
      end
    end
  end

  def get_config
    @title = File.file?(@working_dir / TITLE_FILE) ? 
      open(@working_dir / TITLE_FILE, 'r') {|f| f.gets.strip.toutf8} :
      DEFAULT_TITLE

    @author = File.file?(@working_dir / AUTHOR_FILE) ?
      open(@working_dir / AUTHOR_FILE, 'r') {|f| f.gets.strip.toutf8} :
      DEFAULT_AUTHOR

    @language = File.file?(@working_dir / LANG_FILE) ?
      open(@working_dir / LANG_FILE, 'r') {|f| f.gets.strip.toutf8} :
      DEFAULT_LANG
  end

  def get_file_list
    raise "TXT folder does not exist" unless File.directory?(@working_dir / TEXT_DIR)
    Dir.glob(@working_dir / TEXT_DIR / TEXT_FILES) {|file|
      if File.file?(file)
        content = open(file, 'r'){|f| f.read}
        next if content.empty?
        name = File.basename(file, ".*").toutf8 # filename without extension
        @file_list << {
          :title => name,
          :content => content.toutf8,
          :relpath => CONTENTS_DIR / name + '.html',
          :id => @uuid_obj.generate
        }
      end
    }
  end

  def convert_hatena
    @file_list.each {|file|
      lines = @hparser.parse(file[:content])
      if lines[0].instance_of?(HParser::Block::Head)
        file[:title] = lines[0].to_text
      end
      xhtml_body = lines.map{|e| e.to_html }.join("\n")
      title = file[:title]
      file[:xhtml] = ERB.new(XHTML_TEMPLATE).result(binding)
    }
  end

  def make_epub
    @zip = Zip::ZipFile.new(@working_dir / ZIP_FILE, Zip::ZipFile::CREATE)
    add_mimetype
    add_container_xml
    add_opf
    add_ncx
    add_css
    add_html
    @zip.close
    @zip = nil
  end

  def check_zip_open
    if @zip.nil?
      raise "Zip file not open"
    end
  end

  def write_file_in_book_dir(relpath, content)
    path = Pathname.new(@working_dir / BOOK_DIR / relpath)
    path.dirname.mkpath
    puts "create file " + path.to_s
    open(path.to_s, "wb") {|f|
      f.print(content)
    }
  end

  def add_file_relative_to_book(relpath, content)
    check_zip_open
    write_file_in_book_dir(relpath, content)
    @zip.add(relpath, @working_dir / BOOK_DIR / relpath)
  end

  def add_mimetype
    add_file_relative_to_book(MIMETYPE_FILE, MIMETYPE_CONTENT)
  end

  def add_container_xml
    add_file_relative_to_book(CONTAINER_XML_FILE, ERB.new(CONTAINER_XML_TEMPLATE).result(binding))
  end

  def add_opf
    add_file_relative_to_book(OPF_FILE, ERB.new(OPF_TEMPLATE).result(binding))
  end

  def opf_html_items
    @file_list.map{|file|
      id = file[:id]
      file = file[:relpath]
      ERB.new(OPF_ITEM_XHTML_TEMPLATE).result(binding)
    }.join('')
  end

  def opf_idrefs
    @file_list.map{|file|
      id = file[:id]
      ERB.new(OPF_IDREF_TEMPLATE).result(binding)
    }.join('')
  end

  def add_ncx
    add_file_relative_to_book(NCX_FILE, ERB.new(NCX_TEMPLATE).result(binding))
  end

  def ncx_nav_points
    @file_list.map{|file|
      id = file[:id]
      title = file[:title]
      file = file[:relpath]
      ERB.new(NCX_NAV_POINT_TEMPLATE).result(binding)
    }.join('')
  end

  def add_css
    css_content = CSS_CONTENT
    if File.exist?(@working_dir / CSS_FILE)
      open(@working_dir / CSS_FILE, 'r') {|f|
        css_content = f.read
      }
    end
    add_file_relative_to_book(CONTENTS_DIR / CSS_FILE, css_content)
  end

  def add_html
    @file_list.each{|file|
      add_file_relative_to_book(file[:relpath], file[:xhtml])
    }
  end
end


begin
  Hatena2ePub.new(ARGV).start
rescue => err
  puts err
end
puts "Press enter to close."
$stdout.flush
$stdin.gets

依存するライブラリ。

使ってみたところ、HParser の仕様やバグによりかなり制限を受ける。

例えば、引用記法の中身はすべてインラインテキストになってしまうため、実際には使えないに等しい。

あと、スーパー pre 記法の言語指定や、[ユーアールエル:title=タイトル] みたいな記法が使えなかったり。これははてな記法が進化したため。

このへんは直したい気もするが、それよりも Markdown や Textile や MediaWiki 記法などライブラリが充実しているものを ePub にするスクリプトを作ったほうが確実だし汎用性高いし良さそうな気がする。

String#/ はどっかのライブラリのパス名クラスで使われてたやつをパクってみた。こういうことが出来る Ruby はおもしろいな。

ePub 形式のメタファイルの冗長さが嫌になるレベル。同じ情報を複数ファイルに書かないといけなかったり。こんなことなら W3C Widget 形式で良かったと思う。

HilarioHilario2013/01/14 10:24Play inofmraitve for me, Mr. internet writer.

zvjedelgoqzvjedelgoq2013/01/14 23:05oU2hpb <a href="http://yvtmwnstxutl.com/">yvtmwnstxutl</a>

fkutucsffkutucsf2013/01/16 12:06jqrCuc <a href="http://zjnnnqnvvuvw.com/">zjnnnqnvvuvw</a>

LetitiaLetitia2016/05/07 18:26great article about a problem which has engulfed some of the wevrwstems…..hoaeber the issue at hand is with low quality EMD’s the high quality content still has its value and will continue to thrive…

KeylonKeylon2016/05/08 22:34Sorry for the huge review, but I’m really loving the new Zune, and hope this, as well as the excellent reviews some other people have written, will help you decide if <a href="http://maszlwzvfi.com">it87#21&;s</a> the right choice for you.

ChubbyChubby2016/05/09 03:58Hehe, samma här! Lite märklig lista, jag har lagt ner max 30min i geoscore och det är det enda i statistken jag kan minnas att jag jobbat på. Script till #seicachwng cachebot har jag lagt ner en hel bunt timmar på. Men det kanske är det som de räknat in under statsen. Men men, lite kul att man fått ett omnämnande där, de små sakerna är sånt som får folk att känna sig uppskattade http://ghwcacjgrfn.com [url=http://xuibws.com]xuibws[/url] [link=http://qiwies.com]qiwies[/link]

JimmyJimmy2016/05/09 10:38Hi Shraddha,Thanks for ur lovely words. I hope that I am able to help change people’s perspectives with my works… and their <a href="http://dbaxsokrkp.com">mittiserprenations</a> / misunderstandings of others through 2nd hand information. Fiction is my way of helping others see and feel the truth for themselves

トラックバック - http://rubyist.g.hatena.ne.jp/edvakf/20110402
 |