電書部技術班、電子書籍配信サーバーに挑む

第4回電書用マークアップYDMLを使った原稿作成と、YDMLパーサ

EPUB/PDFへの変換を自前で行う

今回は、電書の原稿フォーマットとその中心になるマークアップ言語YDMLの概要、そしてそのパースについて説明します。

電書部では当初から、原稿からEPUB/PDFへの変換を自前で行う方針でした。したがって、原稿フォーマットの要件も自分たちで決めなくてはなりません。一つの原稿からPDF/EPUBの両方を自動的に生成できることの他に、何が必要でしょうか。

飲み会や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         # 目次情報

電書情報

著者・書名などの情報を、okuduke.txtというファイルに書きます。

okuduke.txtの例
<title>電書の作り方</title>
<short_name>densho_sample</short_name>
<persons>電子書籍部</persons>
<staff>
構成・テキスト:XXX
グラフィックス:XXX
写真:XXX
</staff>
<date>2010/04/30</date>
<publisher>電子書籍部(米光一成)</publisher>

short_nameは、サーバ内で電書を識別するために使われます。

本文ファイルの名前

本文になるファイルは先頭に連番をつけます。拡張子はtxtです。

ファイル名の例
00_maegaki.txt
01_genkou.txt
02_genkou.txt
03_atogaki.txt
04_writers.txt

表紙にしたいページがあれば、そのページはcover.txtと名付けます。本文や表紙の内容はプレーンテキストでもかまいませんし、後述のマークアップを含んでもかまいません。

目次情報

EPUBリーダで表示する目次は、各テキストの1行目から自動的につくります。でも、それでは困る場合もあります。そういうときは、目次情報を持つファイルtoc.txtを用意します。

toc.txtの例
00_maegaki.txt, 前書き
01_genkou.txt, この本に書かれていること
04_writers.txt, 執筆者プロフィール

一行にファイル名と対応する見出しをカンマで区切って並べます。対応する見出しがないファイルには、目次が作られません。

ファイルへのマークアップ

本文ファイルおよびcover.txtの中では、簡単なマークアップ言語を使うことができます。以下で説明します。

電書マークアップ言語「YDML」

電書マークアップの例を次に示します。

YDMLで書かれたテキスト(⁠⁠電子書籍宣言』から引用。『マガジン航』に冒頭部分が掲載されている)
<bold>【世界初!?の電子書籍フリマ】</bold>
<center><img="01.jpg"></center>
<center>米光・小沢</center>

<bold>小沢 </bold>電子書籍フリマってのをやるんでしょ?
<bold>米光 </bold>今年(2010年)の夏と秋に。5/23に文学フリ
マで電子書籍の販売をやるので、その成果を発展させるつもり。「デジタルで
バーチャルな電書をアナログでリアルな対面販売で」ってお祭りをやろうと思
ってる。
<bold>小沢 </bold>「電子書籍フリマをやろう」というアイデ
アは、そもそもどこから?
<bold>米光 </bold>去年の10月にキンドルを手に入れてから、
これでいったいどんなことができるんだろう、何が変わるんだろう、っていう
のをずっと考えていて。

電書部技術班ではこのマークアップを「YDML(Yonemitsu Densho Markup Language⁠⁠」と呼んでいます。

タグ一覧

YDMLのタグは<>で囲まれた文字列です。閉じタグはXMLやHTMLと同じように </ からはじまります。

YDMLのタグ一覧
タグ閉じタグ説明
<left>あり左寄せになる。
<center>ありセンタリングされる。
<right>あり左寄せになる。
<large>あり大きなフォントになる。
<small>あり小さなフォントになる。
<quote>あり引用扱いになる。
<bold>あり太字になる。
<sonomama>あり囲まれたテキスト内のYDMLタグが、そのまま表示される。
<ruby>ありルビをふる。たとえば、<ruby>蜻蛉/とんぼ</ruby>のように使う。
<bouten>あり傍点をふる。
<img>なしイメージを表示する。ファイル名は<img="1.jpg">のように指定。また、<img="1.jpg" size="80%">としたときは、リーダの表示幅に対して、指定した比率の幅でイメージを表示する。sizeを指定しない場合は、30%で表示する。
<hasen>なし破線を引く。

YDMLの考え方

YDMLはレイアウト指向で、文章の論理構造をマークアップするものではありません。そうなったのは、次のことが理由です。

レイアウトは、著者がみずから行いたい。
著者の意に反したレイアウトが自動的に決まるようにはしたくない、という考えです。かといって、cssのようなものを採用すると「誰にでも書ける」という目標から遠ざかります。
文章の要素は、すべて論理的に意味が決まるとは限らない。
「この章見出しっぽいものは、実は本文でもある」という表現だって、ありえます。

実は<quote>タグだけはレイアウト指向ではないマークアップになってしまっています。⁠字下げ」タグにすべきだったかもしれません。

YDMLのパース

YDMLは簡単に書けますが、パースしづらいフォーマットです。プレーンテキストに時々タグが混じる形なので、XMLパーサではパースできません。Wiki記法のような行指向でもないため、改行を利用したパースもできません。

現在の電書変換では、正規表現をつかってターゲットのフォーマットに直接置換する方法を使っています。EPUBに変換しているコードの一部を以下に示します。

YDMLをHTMLタグに変換するrubyコードの一部
      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の拡張に手間がかかる

あたらしいパーサを作ろう

これらの問題を解決するために、YDMLの新しいパーサを実装中です。まずパーサジェネレータをつくり、その上でYDMLを定義するアプローチをとっています。

パーサジェネレータ(Noratextという名前をつけました)は、次のことを目指しています。

  • テキストに所々混じるタイプのマークアップを簡単に定義・パースできること。
  • パース結果から、複数のフォーマットに簡単に変換できること。

現在のNoratextには3つの機能があります。

  1. 語彙の定義からレキサを生成する
  2. 文法の定義からパーサを生成する
  3. 生成規則の定義から変換コードを生成する

最新版は http://github.com/skoji/Noratext にあります。執筆時点で完成しておらず仕様も確定していないので、ここではNoratextの仕様や実装には立ち入らず、YDML定義の概要を説明をしていきます。

語彙定義

Noratextを使ったYDMLの語彙定義を次に示します。

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に定義した名前で、<>で囲まれた文字がタグとして認識できるlexerが定義されます(現バージョンのNoratextは<>で囲まれたタグの定義のみに対応しています⁠⁠。定義した後は次のように使います。

lexer使用例
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によるYDMLの文法定義の一部を示します。

noratextによるYDMLの文法定義(一部)
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

これにより、YDML文書の構造を読み取れるパーサが定義されます。使い方は、次のとおりです。

parser使用例
# まず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を生成する規則の定義

そして、最終的なフォーマットを生成する規則の定義です。YDMLからXHTMLを生成する規則定義の一部を次に示します。

noratextによるYDML Processor定義の一部
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というオブジェクトをつくり、そこにparserのパース結果(ParsedDataオブジェクト)を読ませる構造になっています。パース結果の要素に一対一対応で出力を生成します。

今後の予定

Noratextはひととおりの機能は実装していますが、まだまだ問題があります。特に「簡単にマークアップを定義できる」とは言い難いのが、一番大きな課題です。例えばparserのtext要素定義はずっとシンプルにできるはずです。do/endが多すぎるparser定義の書き方も気にかかります。まずはNoratextを利用したYDMLパーサを完成させますが、その後も「簡単に定義」を目指して大幅に変更していこう、と考えています。

なお、YDMLパーサそのものは、執筆時点ではまだ公開していません。EPUB変換とあわせて動作するようになり次第、githubで公開する予定です。

Noratextという名前

最後に、パーサジェネレータの名前について説明します。

「だれかによって権威づけされたテキストではなく、あたりを自由にうろつく"野良テキスト"が電書になる助けをする」というイメージを持って、Noratextという名前を選びました。新パーサを作ろうと考えてはじめた2010年7月ごろです。

同じ頃に初代電書部長・米光一成は、ぼくにとっては、電書は「黒船」なんかじゃなくて、街を自由に巡る「野良猫」のような存在と言っています。ここからNoratextという名前をとったようにみえるかもしれませんが、違います。Noratextをはじめたとき、私はこの言葉を知りませんでした。

とはいえ、⁠野良テキスト」という発想は、電書部活動を通して知った米光さんの考え方に明らかに影響を受けています。ですから、⁠電書は街を自由に巡る野良猫のような存在」って言葉からNoratextと名付けた、といってしまってもいいかもしれません。

おすすめ記事

記事・ニュース一覧