そういう需要があったので作ってみた。一応メモとして残しておく。
その README から抜粋。(hatena2epub.bat というのは hatena2epub.rb にカレントディレクトリを引数として渡すだけのバッチファイル)
Hatena2ePub
はてな記法で書かれた複数のテキストファイルをまとめて ePub にします。
使い方
- hatena2epub.bat と同じフォルダ内に TXT というフォルダを作ります。
- その中に、はてな記法のテキストを .txt という拡張子で保存します。
- hatena2epub.bat をダブルクリックして実行します。
- DOS ウィンドウが立ち上がり、成功すれば BOOK.epub というファイルが出来ます。
その他
もし hatena2epub.bat と同じフォルダ内に、以下のようなファイルがあれば、それを設定として適用します。
ファイル名 説明 デフォルト設定 STYLE.css ePub の CSS 空の 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 '<' '<' when '>' '>' when '"' '"' when '&' '&' 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 形式で良かったと思う。