エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

Markdown から EPUB を作る

Markdown から EPUB を生成する方法の備忘録です。

Markdown から EPUB に変換するなら、md2review + Re:VIEW という強力な組み合わせがあります。

…が、今回は gepub という gem を使って生成してみました。 ここにあげたコードは gist.github.com に全体をアップしています。

gem を用意する

今回利用する gem、ライブラリです。

  • redcarpet
    • Markdown から HTML への変換に利用します。
  • nokogiri
    • HTML から XHTML への変換と要素の編集に利用します。
  • securerandom
    • EPUB に設定する identifier の値を生成するために利用します。
  • gepub
    • EPUB の生成に利用します。
require 'redcarpet'
require 'nokogiri'
require 'securerandom'
require 'gepub'

Markdown を分割する

今回はレベル 1 の見出しを各章の始まりとして扱うようにしました。 そのためまず一つの Markdown を章単位に分割していきます。 なおこのコードでは、レベル 1 の見出しがないケースや、レベル 1 の見出しと見出しの間に文章が含まれないようなケースは考慮してません。

Chapter = Struct.new("Chapter", :title, :body)

def split_md_into_chapters(source)
  result = []
  title = nil
  begin
    body, next_title, rest = source.partition(%r{^# .*\n})
    result << Chapter.new(title, "# #{title}\n#{body}") unless body.empty?
    title, source = next_title[2..-2], rest
  end until source.empty?

  result
end

各章を XHTML に変換する

redcarpet を利用して Markdown から HTML に変換し、nokogiri を利用して HTML から XHTML に変換しています。 redcarpet にも XHTML 形式で出力する機能がありますが、EPUB で利用するには厳密に XML の仕様にそっている必要があるようです。 また EPUB のビューアによっては title 要素が含まれていないと正しく表示できないようです。ここではレベル 1 の見出しの内容をタイトルとして設定しています。

def convert_to_xhtml(chapter)
  markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML.new, autolink: true, tables: true, fenced_code_blocks: true, prettify: true)
  html = Nokogiri::HTML.parse( markdown.render(chapter.body))
  html.title = chapter.title
  html.to_xhtml
end

XHTML をまとめて EPUB で出力する

gepub を利用して EPUB を生成します。 ここで identifier の設定は必須になっていますが空文字列でも生成できました。ただしビューアによっては identifier が空文字列になっていると正しく表示できないようです。

画像ファイルを含めたい場合は book.ordered ブロックの外で book.add_item を利用して追加します。このとき指定するパスはテキスト内から参照しているパスと合わせる必要があります。 また各ファイルを格納するパスは book.add_item("#{index}.xhtml") のように直下に配置してしまうと、ビューアによってはうまく参照できないようで、コードのようにサブディレクトリに配置するのがよさそうです。

book.generate_epubEPUB をファイルに出力しています。 book.generate_epub_stream を利用するとファイルでなくストリームとして結果を取得できます。このとき結果として返るストリームはカーソルが終端に移動したままになっているようなので、seek(0) で先頭に移動してから読み出す必要があります。

def generate_epub_from_markdown(source)
  book = GEPUB::Book.new {|book|
    book.identifier = "md2epub-#{SecureRandom.uuid}"
    book.title      = 'md2epub - Markdown から EPUB を作る'

    book.ordered do
      split_md_into_chapters(source).each_with_index do |chapter, index|
        xhtml = convert_to_xhtml(chapter)

        book.add_item("text/#{index}.xhtml")
            .add_content(StringIO.new(xhtml))
            .toc_text(chapter.title)
      end
    end
  }

  book.generate_epub('md2epub.epub')
end

実行

簡単な Markdownepub に変換してみます。

generate_epub_from_markdown(<<~MARKDWON)
# Markdown を分割する

レベル 1 の見出しを一つの章として扱うように分割します。

# 各章を XHTML に変換する

gem [redcarpet](https://github.com/vmg/redcarpet) と [nokogiri](https://www.nokogiri.org) を利用して markdown から XHTML に変換します。

```ruby
markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML.new, autolink: true, tables: true)
html = Nokogiri::HTML.parse(markdown.render(source))
html.title = title
xhtml = html.to_xhtml
```

# XHTML をまとめて EPUB で出力する

gem [gepub](https://github.com/skoji/gepub) を利用して XHTML から EPUB に変換します。
MARKDWON

生成された EPUB ファイルを Mac 標準の Books で開くとこのように表示されます。

いつか読むはずっと読まない:Wunderkammer

もう展示は終わってしまいましたが、とても興味深い内容でした。

企画展「標本づくりの技(ワザ)-職人たちが支える科博-」

へんなものみっけ! (1) (ビッグコミックス)

へんなものみっけ! (1) (ビッグコミックス)

へんなものみっけ! (2) (ビッグコミックス)

へんなものみっけ! (2) (ビッグコミックス)

へんなものみっけ! (3) (ビッグコミックス)

へんなものみっけ! (3) (ビッグコミックス)