EPUB/PDFへの変換を自前で行う
今回は、
電書部では当初から、
飲み会やSkypeチャットで検討や議論を行い、
- ルールが簡単で、
誰にでも書ける。 - 技術班の助けをかりなくても、
著者が見栄えを直接コントロールできる。 - 見栄えは新書や文庫程度のレベルで十分で、
雑誌のようなリッチなものは不要。
電書部の原稿フォーマット
これらの要件を満たす、
ディレクトリ構造
ディレクトリを一つ用意し、
densho_sample/ # 任意のディレクトリ
00_maegaki.txt # 本文原稿
01_genkou.txt
02_genkou.txt
03_atogaki.txt
04_writers.txt
cover.txt # 表紙原稿
cover.jpg # 画像ファイル
capture.jpg
okuduke.txt # 書籍情報
toc.txt # 目次情報
電書情報
著者・
<title>電書の作り方</title>
<short_name>densho_sample</short_name>
<persons>電子書籍部</persons>
<staff>
構成・テキスト:XXX
グラフィックス:XXX
写真:XXX
</staff>
<date>2010/04/30</date>
<publisher>電子書籍部(米光一成)</publisher>
short_
本文ファイルの名前
本文になるファイルは先頭に連番をつけます。拡張子はtxtです。
00_maegaki.txt
01_genkou.txt
02_genkou.txt
03_atogaki.txt
04_writers.txt
表紙にしたいページがあれば、
目次情報
EPUBリーダで表示する目次は、
00_maegaki.txt, 前書き
01_genkou.txt, この本に書かれていること
04_writers.txt, 執筆者プロフィール
一行にファイル名と対応する見出しをカンマで区切って並べます。対応する見出しがないファイルには、
ファイルへのマークアップ
本文ファイルおよびcover.
電書マークアップ言語「YDML」
例
電書マークアップの例を次に示します。
<bold>【世界初!?の電子書籍フリマ】</bold>
<center><img="01.jpg"></center>
<center>米光・小沢</center>
<bold>小沢 </bold>電子書籍フリマってのをやるんでしょ?
<bold>米光 </bold>今年(2010年)の夏と秋に。5/23に文学フリ
マで電子書籍の販売をやるので、その成果を発展させるつもり。「デジタルで
バーチャルな電書をアナログでリアルな対面販売で」ってお祭りをやろうと思
ってる。
<bold>小沢 </bold>「電子書籍フリマをやろう」というアイデ
アは、そもそもどこから?
<bold>米光 </bold>去年の10月にキンドルを手に入れてから、
これでいったいどんなことができるんだろう、何が変わるんだろう、っていう
のをずっと考えていて。
電書部技術班ではこのマークアップを
タグ一覧
YDMLのタグは<>で囲まれた文字列です。閉じタグはXMLやHTMLと同じように </ からはじまります。
タグ | 閉じタグ | 説明 |
---|---|---|
<left> | あり | 左寄せになる。 |
<center> | あり | センタリングされる。 |
<right> | あり | 左寄せになる。 |
<large> | あり | 大きなフォントになる。 |
<small> | あり | 小さなフォントになる。 |
<quote> | あり | 引用扱いになる。 |
<bold> | あり | 太字になる。 |
<sonomama> | あり | 囲まれたテキスト内のYDMLタグが、 |
<ruby> | あり | ルビをふる。たとえば、 |
<bouten> | あり | 傍点をふる。 |
<img> | なし | イメージを表示する。ファイル名は<img="1. |
<hasen> | なし | 破線を引く。 |
YDMLの考え方
YDMLはレイアウト指向で、
- レイアウトは、
著者がみずから行いたい。 - 著者の意に反したレイアウトが自動的に決まるようにはしたくない、
という考えです。かといって、 cssのようなものを採用すると 「誰にでも書ける」 という目標から遠ざかります。 - 文章の要素は、
すべて論理的に意味が決まるとは限らない。 - 「この章見出しっぽいものは、
実は本文でもある」 という表現だって、 ありえます。
実は<quote>タグだけはレイアウト指向ではないマークアップになってしまっています。
YDMLのパース
YDMLは簡単に書けますが、
現在の電書変換では、
line.gsub!('<left>', '<div class="left">')
line.gsub!('<center>', '<div class="center">')
line.gsub!('<right>', '<div class="right">')
line.gsub!(/(<\/left>)|(<\/center>)|(<\/right>)/, '</div>')
line.gsub!('<large>','<span class="large">')
line.gsub!('<small>','<span class="small">')
line.gsub!('<bold>','<span class="bold">')
line.gsub!(/(<\/large>)|(<\/small>)|(<\/bold>)/, '</span>')
if (@ruby_enable)
line.gsub!(/<ruby>(.+?)\/(.+?)<\/ruby>/, '<ruby>\1<rp>(</rp><rt>\2</rt><rp>)</rp></ruby>')
else
line.gsub!(/<ruby>(.+?)\/(.+?)<\/ruby>/, '\1(\2)')
end
このやりかたには、
- 閉じタグ忘れなどのエラーが検出できない
- EPUB変換とPDF変換で別々のコードになってしまうため、
同じ原稿から別のトラブルが発生することがある - YDMLの拡張に手間がかかる
あたらしいパーサを作ろう
これらの問題を解決するために、
パーサジェネレータ
- テキストに所々混じるタイプのマークアップを簡単に定義・
パースできること。 - パース結果から、
複数のフォーマットに簡単に変換できること。
現在のNoratextには3つの機能があります。
- 語彙の定義からレキサを生成する
- 文法の定義からパーサを生成する
- 生成規則の定義から変換コードを生成する
最新版は http://
語彙定義
Noratextを使ったYDMLの語彙定義を次に示します。
Noratext::Lexer.define :ydml do
symbols :center, :left, :right, :large, :small, :bold, :bouten, :quote, :image, :ruby, :sonomama, :hasen
# 名前とマッチパターンが異なるタグ
match_pattern :image, 'img=".+?"'
# 閉じタグのないタグ
without_close :image, :hasen
# 属性のあるタグ
add_parser :image do
|s|
/<img="(.*?)"/ =~ s
imgpath = $1
/size="(.*?)"/ =~s
size = $1
{ :imgpath => imgpath, :size => size }
end
# 閉じタグまで全部プレーンテキストとして読み取るタグ
rawtext_till_close :sonomama
end
symbolsに定義した名前で、
text = 'テストのテキスト。<quote>引用された文字列。</quote><img src="gazou.jpg" size="50%">'
lexer = Noratext::Lexer.generate(:ydml)
result = lexer.process(StringIO.new(text))
result[0][:type] # => :text
result[0][:data] # => 'テストのテキスト。'
result[1][:type] # => :quote
result[1][:tag][:kind] # => :opentag
result[2][:type] # => :text
result[2][:data] # => '引用された文字列'
result[3][:type] # => :quote
result[3][:tag][:kind] # => :closetag
result[4][:type] # => :image
result[4][:tag][:imgpath] # => "gazou.jpg"
result[4][:tag][:size] # => "50%"
Lexer#processを呼ぶことで、
文法定義
次に、
Noratext::Parser.define :ydml do
element :document do
contains :block
end
element :block do
is_oneof :paragraph, :center, :left, :right, :quote, :hasen
end
element :paragraph do
contains :text, :large, :small, :bold, :image, :sonomama, :ruby
end
element :center do
open_close
contains :paragraph
end
element :text do
accepts :text
parse_sequence do
|sequence|
data = ""
while (sequence.size > 0 &&
sequence[0][:type] == :text)
data << sequence.shift[:data]
end
{ :data => data }
end
end
end
これにより、
# まずlexerを利用
text = "テストの<bold>太字テキスト</bold>。<quote>引用された文字列</quote>"
lexer = Noratext::Lexer.generate(:ydml)
sequence = lexer.process(StringIO.new(text))
# lexer結果をパース
parser = Noratext::Parser.generate(:ydml)
parsed = parser.parse(sequence)
parsed.type # => :document
parsed.is_leaf? # => false
parsed.children[0].type # => :paragraph
parsed.children[0].children[0].type # => :text
parsed.children[0].children[0].data # => "テストの"
parsed.children[0].children[1].type # => :bold
parsed.children[0].children[1].children[0].type # => :text
parsed.children[0].children[1].children[0].data # => "太字テキスト"
parsed.children[1].type #=> :quote
...
documentを根とした、
XHTMLを生成する規則の定義
そして、
module YDML
class ToHTML < Noratext::Processor
def initialize(io)
if !io.nil?
# YDML::Parser::parseでは内部でNoratextのLexerとParserを続けて呼ぶ。
@parsed = YDML::Parser.parse(StringIO.new(Kconv.toutf8(io.read).gsub(/\r\n?/, "\n")))
@parsed.log.each {
| x |
warn "line #{x[:lineno]} : #{x[:log]}\n"
}
end
openclose_element :document do
open <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml" lang="ja" xml:lang="ja">
<head>
<title>#{@title}</title>
<link rel="stylesheet" type="text/css" href="#{@csspath}"></link>
</head>
<body>
EOF
close "</body></html>"
end
openclose_element :paragraph do
open "<p>"
close "</p>"
end
openclose_element :center do
open '<div class="center">'
close '</div>'
end
element :text do
proc do
|elem|
elem.data.gsub("\n", "<br />")
end
end
end
def tohtml
process(@parsed)
end
end
Processorというオブジェクトをつくり、
今後の予定
Noratextはひととおりの機能は実装していますが、
なお、
Noratextという名前
最後に、
「だれかによって権威づけされたテキストではなく、
同じ頃に初代電書部長・
とはいえ、