golden-luckyの日記

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

LaTeXソースを出力するときのエスケープ

昨日までの記事では、XMLの構文で書かれた原稿を他のマークアップにどうやって変換しているかを紹介しました。 こういった変換をするときに一般に悩みの種になるのが、変換先の記法で特殊な意味を持つ文字の扱いです。

たとえばTeXでは、次の10種類の文字は「原稿の入力にそのまま使えない」とされています。 最終的な印字結果にこれらを出力したい場合には、原稿上で何らかの「処置」が必要です。

\ { } $ & # ^ _ % ~

今日は、これらをLaTeXのソースでどうエスケープしたらいいか、という話です。

TeXエスケープ文字は文字のエスケープをする文字ではない

プログラミング言語などで特殊な文字を入力したい場合、一般には「エスケープ文字を前置する」という方法を使います。 エスケープ文字としてお馴染みなのは、バックスラッシュ記号「\」でしょう。 "\n"とか、"\\"とか、"\""といったやつです。

TeXにも「エスケープ文字」があり、それは一般にバックスラッシュとされています。 そのため、上記の特殊な文字にとりあえずバックスラッシュを前置し、意図した文字と違う文字が出て悩んだ人も多いのではないでしょうか?

TeXでバックスラッシュを特殊な文字に前置しても意図した文字が印字されるとは限らないのは、 これがCやJavaScriptなどの汎用のプログラミング言語で「文字」をエスケープするための「エスケープ文字」とはちょっと毛色が違うからです。

とはいえ、通常のTeXでもバックスラッシュは確かに「エスケープ文字」なので、そりゃあ混乱しますよね。

実はTeXのバックスラッシュは、「後続の特殊な文字をその文字そのものにする」という意味でエスケープ文字なのではなく、「後続の文字たちを命令として扱う」ためのエスケープ文字です。 要するに、TeXの字句解析では、エスケープ文字に続く文字を文字としては読み取らず、常に命令の名前を構成する先頭の要素として読みます。 結果、命令の名前がたまたま1文字で、その命令がその文字を表すような印字結果を導くものであれば、いわゆる「エスケープ文字」に期待される動作になります。 そうでなければ、そうなりません。

なので、TeXのソースを吐き出す仕組みでは、わりとみんな苦労して「エスケープ」の問題にあたっているようです。 よく見るのは、「ふつうにバックスラッシュでエスケープできない文字は強引に1文字の名前の命令にする」方法でしょう。 \\^{}のように、波カッコの空ブロックを後置した文字列を変換結果の出力とする、という手法です。 また、TeXの側の仕組みであらかじめ定義された「文字を表す命令」を出力するという手もあります。

たとえばPandocのLaTeXライターは、上記の10個の文字を、それぞれこんな感じに現状ではエスケープしているようです。

case x of
  '{' -> "\\{" ++ rest
  '}' -> "\\}" ++ rest
  '$' | not isUrl -> "\\$" ++ rest
  '%' -> "\\%" ++ rest
  '&' -> "\\&" ++ rest
  '_' | not isUrl -> "\\_" ++ rest
  '#' -> "\\#" ++ rest
  '~' | not isUrl -> "\\textasciitilde{}" ++ rest
  '^' -> "\\^{}" ++ rest
  '\\'| isUrl     -> '/' : rest
      | otherwise -> "\\textbackslash{}" ++ rest

どんな環境で出現するかによって処理が変わっているのでごちゃごちゃしてますが、ノリはわかると思います。 にしても、URLの中だとバックスラッシュを「/」に変換してしまうというのは、やりすぎな気もしますが…。

\symbolを使う

マークアップにおいて特殊な文字を使うのに、エスケープ文字の前置以外の手法が用意されている場合があります。 HTML文字実体参照とか、ASCIIエスケープシーケンスみたいなやつです。 この言い方が正しいか微妙ですが、特殊な文字のための専用の文法がマークアップ側に用意されていると考えてもいいと思います。

で、LaTeXにもそれに近い機能の命令があります。それは\symbol{数字}です。 数字の部分に、そのときのフォントエンコーディングにおける文字を示す数字を指定することで、その文字のグリフが印字される仕組みです。

昨日までの記事で紹介したxml2texでは、この方法でLaTeXマークアップにおける特殊な文字を「エスケープ」しています。 具体的には、TeXのソースにあると扱いが面倒な記号を、すべてTeX\symbol{数字}に変換しています。 こんな感じです。

(define (tex-escape str)
  (regexp-replace-all 
   #/[(]symbol (\d{1,3})[)]/
   (regexp-replace-all* str
      #/\\/ "(symbol 92)"
      #/{/  "(symbol 123)"
      #/}/  "(symbol 125)"
      #/\#/ "(symbol 35)"
      #/\$/ "(symbol 36)"
      #/\%/ "(symbol 37)"
      #/\&/ "(symbol 38)"
      #/\_/ "(symbol 95)"
      #/\^/ "(symbol 94)"
      #/\~/ "(symbol 126)")
   "{\\\\symbol{\\1}}"))

正規表現を2段階にしているのは、\symbol{数字}の中に出現するバックスラッシュと波カッコが再置換されるのを防ぐためです。 元の原稿に「(symbol 92)」のような文字列が出現する場合のことは、このtex-escapeという関数では想定していません。 その可能性がある場面では別のエスケープ用の関数を定義して使います。

注意が必要なのは、この数字はフォントエンコーディングによって異なるということです。 そのときのフォントエンコーディングLaTeXの層の話なので、XMLからLaTeXへの変換では決められません。 逆にいうと、何かしら決め打ちしてるということです。 xml2texの場合は、T1というエンコーディングに決め打ちしています。 ここで昨日の記事のデフォルトの出力結果の最初の3行を思い出してもいいでしょう。

\documentclass{book}
\usepackage[T1]{fontenc}
\usepackage{alltt}

デフォルトがこうなっていたのは、このへんの事情によります。 allttをデフォルトで呼び出しているのも、古き良きverbatimだと\symbolによる特殊文字の印字が使えないからです。

このようにいくつか制約はありますが、経験上、この方法でLaTeXへの変換におけるエスケープをするのがいちばん困ることが少ないように思います。 ちなみに自分は、Pandocでも、コードブロックの構造に対して以下のような上記と同等なエスケープ処理を施すようにしています。

escapeTeX :: String -> String
escapeTeX s = 
  let re  = R.mkRegex "[(]symbol ([0-9]{1,3})[)]"
  in R.subRegex re (at $ sh $ bs $ lp $ rp $ us $ ps $ dl $ ad s) "{\\symbol{\\1}}"
  where bs s = R.subRegex (R.mkRegex "[\\]") s "(symbol 92)"
        lp s = R.subRegex (R.mkRegex "{") s "(symbol 123)"
        rp s = R.subRegex (R.mkRegex "}") s "(symbol 125)"
        at s = R.subRegex (R.mkRegex "[@]") s "(symbol 64)"
        sh s = R.subRegex (R.mkRegex "[#]") s "(symbol 35)"
        us s = R.subRegex (R.mkRegex "[_]") s "(symbol 95)"
        ps s = R.subRegex (R.mkRegex "[%]") s "(symbol 37)"
        dl s = R.subRegex (R.mkRegex "[$]") s "(symbol 36)"
        ad s = R.subRegex (R.mkRegex "[&]") s "(symbol 38)"

Pandocでは、Writerの前段にPandocフィルターをかませることができます。 最初に示したPandoc標準のエスケープ処理のコードはLaTeX用のWriterのものなので、コードブロックの構造に対する処理をフィルターで奪うことにより、上記の独自エスケープ処理を済ませた結果が吐き出されるという仕掛けです。

明日からはPandocまわりの話を始めるつもりです。 が、まだ構想が決まってないので、もう少し小ネタを挟むかかも。