golden-luckyの日記

ツイッターより長くなるやつ

Pandocの抽象データ型

Pandocのいいところは、構造をさまざまな記法から暗黙に読み取ってくれる点です。 ただし、その構造はPandocの内部で定義された抽象データ型であり、利用者の目的に合わせて増改築することはできません。 XMLLaTeXでやるフルフルの構造化文書に比べると良い意味でも悪い意味でも制限があるので、昨日の記事ではPandocによる構造化文書の扱いを「ライトウェイト」と呼びました。

今日は、この「抽象データ型によるライトウェイト構造化文書」について、Pandocの抽象データ型を例にもうちょっと具体的に説明します。

Pandocの抽象データ型

なにはともあれ、Pandocの抽象データ型がどんなかを見てみましょう。

Pandocにおける文書とは、「ブロック要素」と「インライン要素」からなるデータです。 この概念の具体的な姿は、Haskellの代数的データ型という仕組みを使って、pandoc-typesというHaskellのパッケージで定義されています。

hackage.haskell.org

このパッケージで定義されているPandocの「型」を簡単に紹介します。 まず、これがPandocのインライン要素を表す代数的データ型です。

data Inline
    = Str Text              -- 単純な文字列
    | Emph [Inline]         -- 強調
    | Strong [Inline]       -- もっと強調
    | Strikeout [Inline]    -- 打ち消しの文字列
    | Superscript [Inline]  -- 上付き文字列
    | Subscript [Inline]    -- 下付き文字列
    | SmallCaps [Inline]    -- スモールキャプスの文字列
    | Quoted QuoteType [Inline] -- 引用の文字列
    | Cite [Citation]  [Inline] -- 文献参照
    | Code Attr Text        -- 等幅の文字列
    | Space                 -- 空白
    | SoftBreak             -- ソフト改行
    | LineBreak             -- ハード改行
    | Math MathType Text    -- TeX記法の数式
    | RawInline Format Text -- HTMLやLaTeXの生データ
    | Link Attr [Inline] Target  -- ハイパーリンク
    | Image Attr [Inline] Target -- 画像
    | Note [Block]          -- 脚注や後注
    | Span Attr [Inline]    -- 汎用のインライン要素

Pandocにおけるもっとも基本的な型のひとつは、素の文字列Textに対してStrという名前を付けたStr Textであり、これがインライン要素を表す型Inlineの代表選手です。 なお、Strのような名前のことを、Haskellでは「構成子(値構成子)」と呼びます。

複数のインライン要素Inlineを集めたものは[Inline]という型で表します。 上記の定義には、「[Inline]Emphなどの構成子を付けたインライン要素」もいくつかあることがわかります。

さらに、いくつかの構成子にはAttrという型の引数もあります。 これは、HTMLとかXMLにおける「属性」に相当する型です。 StrAttrを取らない構成子なので、素の文字列には属性が付けられません。 こうした要素に属性を付けたい場合や、いくつかのインライン要素を束ねて特に意味を持たないインライン要素にしたい場合には、Spanを使います。 これはHTMLの<span>に相当します(余談ですが古代のPandocにはSpanがなくてわりと苦労しました)。

次は、Pandocのブロック要素を表す代数的データ型です。

data Block
    = Plain [Inline]        -- 段落ではないブロック
    | Para [Inline]         -- 段落
    | LineBlock [[Inline]]  -- 複数行からなるブロック
    | CodeBlock Attr Text   -- コードブロック
    | RawBlock Format Text  -- HTMLやLaTeXの生データ
    | BlockQuote [Block]    -- 引用のブロック
    | OrderedList ListAttributes [[Block]]  -- 順序付き箇条書き
    | BulletList [[Block]]                  -- 箇条書き
    | DefinitionList [([Inline],[[Block]])] -- 見出し付き箇条書き
    | Header Int Attr [Inline] -- 見出し
    | HorizontalRule           -- 水平線
    | Table [Inline] [Alignment] [Double] [TableCell] [[TableCell]]  -- 表
    | Div Attr [Block]       -- 汎用のブロック要素
    | Null                   -- 何も含まないブロック

多くのブロック要素は、「インライン要素の集まり[Inline]に構成子を付けたもの」として定義されているのが見て取れると思います。 箇条書き(OrderdListとかBulletListとか)のように、他のブロック要素の集まり[Block]によって構成されているブロック要素もありますね。

以降では、このBlockInlineで定義されたPandocの抽象データ型のことを、「Pandoc構造」と呼びます。

Pandoc構造に当てはまらないものはどうなるか

原稿をPandocで読み込むと、その原稿に含まれている内容は、元の原稿の記法が何であれ、すべてPandoc構造として保持されます。

しかし、たとえば元の原稿がDocBookであったとして、DocBookのスキーマには、このPandocの代数的データ型では表現できないような構造もたくさんあります。 たとえば、初出の用語を表す目的で用意されている構造としてDocBookには<firstterm>というものがありますが、これに相当する構成子はPandocのInline型にはありません。 組版された表示結果で何らかの強調をするといった効果を期待したいだけならEmphマッピングできそうな気もしますが、元の構造は索引項目の抽象などにも使えるように意図されているので、どうやっても別ものにしかならないでしょう。

また、<sidebar>というブロック要素もPandoc構造には対応するものがありません。 これは、いわゆる「コラム」のように、本文とは切り離された余談などを記述するために使うXML要素です。

では、DocBookの原稿に出てくる<firstterm><sidebar>で囲まれた部分は、Pandocではどのように扱われるのでしょうか? 残念ながら、Pandocでは、そもそもこれらのタグを認識してくれません。 単純に要素が無視されるわけではなく、知らないタグがスルーされます。 つまり、その中に出てくる文字列や他のXML要素は読み取って、それをできるだけPandoc構造にマッピングしようと試みます。

なお、上記では未知のタグはスルーされると言いましたが、これはDocbookを読むときの話で、他の記法を読むときにもスルーされるとは限りません。 Pandocでは入力の記法をパースして代数的データ型として読み込む仕組みのことを「Reader」と呼んでおり、さまざまな記法ごとにReaderが用意されています。 そして、そのReaderの実装によって、記法からPandoc構造への対応も異なるからです。 DocBookのReaderの方針がたまたま「知らないタグはスルー」なのであって、他のReaderがどういう方針で実装されているかはまちまちです。 たとえばreSTのReaderは未知のディレクティブを無視します(記法で定まる構造を無視することと、記法をスルーすることは、違う)。

Readerの方針を知るには、実装を見るしかありません。 DocBookのReaderの実装は以下で参照できます。

DocBookの場合には、このReaderの冒頭に、対応するタグと対応しないタグを示したチェックシートが並んでいます。 それを見るとわかるように、ほとんどのタグは未対応のままです。 たぶん今後も完全に対応することはないでしょう。

PandocではDocBookに対応しているとされていますが、このReaderの実装を見ると、処理結果が元のDocBookの原稿で意図されていたものになるとは限らない(むしろならないことのほうが多い)と考えるほうがよさそうだとわかると思います。

構造化文書から記法を分離しないとPandocの話がしにくい

DocBookは、OASISという団体が管理しているガチガチの構造化文書の仕組みです。

昨日の話で構造化文書から記法を分離し、「Pandocは記法の変換」という話をしたのは、今日の話のような状況、つまりPandocが何らかの構造化文書に対応しているといってもそれは標準化されたフルフルの規格に対応しているという意味ではなく、あくまでも「記法からPandoc構造が得られるよ」、もしくは「Pandoc構造から記法が得られるよ」という意味であり、ライトウェイトに構造化文書をやる仕組みであることを強調したかったからです(余談ですが、プログラミング言語の型がライトウェイトな形式手法と呼ばれていることを意識してライトウェイトと言っています)。

ここで、昨日の最後の図を再掲します。 この図は、それぞれの構造化文書の標準的な利用方法を横方向の流れで示したとして、Pandocにおける「対応」はあくまでも「Pandoc構造という抽象データ型に照らして記法を読み書きできる」という意味だ、というふう読んでほしかったものです。

f:id:golden-lucky:20191214182222p:plain

ところで、いままでWriterの話をしてませんが、実はWriterの実装について気にすることはあまりありません。 というのは、Pandocにはフィルターという仕組みがあり、これによって「いったんPandoc構造になったものを何らかの記法で出力する」際にはカスタマイズが可能だからです。 もちろん、Pandoc構造として読み取られていない元の原稿の記法にあった情報を復元することはできませんが、読み取られた範囲で好きなように出力を組み立てることは容易です。

逆に、ある記法の原稿が「Pandoc構造として読み取られる方法」を変えたい場合には、Readerを改造して対処するしかないとも言えます。 明日はそのような事例を紹介します。