天泣記

2022-09-12 (Mon)

#1 LaTeX の minted で、listings の literate 相当のことを行う

LaTeX の listings パッケージや minted パッケージはソースコードをハイライト (色付け) したりしてみやすく表示するものである

ここで listings は TeX でぜんぶ実装されているのに対し、minted は pygments という外部ツールを使う (そのため、minted を使うには、latex の実行に -shell-escape オプションが必要になる)

出力は minted のほうがきれいだといわれているようである (たとえば、Ruby の文字列に埋め込む式は、listings では文字列の一部としてハイライトされるけれど、minted では式としてハイライトされる)

ところで、listings には literate という機能があって、文字列を置換することができる。 たとえば、forall という単語を $\forall$ にしたり、-> という ASCII文字の矢印を $\rightarrow$ にしたりできる。 これは出力がコンパクトになるし、数学の記法の矢印の代用として -> を使っているのが正しい文字になるのがよい

残念ながら minted には literate に相当するような機能は提供されていない (と思う)

しかし調べてみると、いくらかがんばれば、literate に相当することを実現できるようだ。 肝心なところは pygments の lexer (RegexLexer) に callback という機能があってソースとは異なる出力を生成できるというところである

まず、pygments は入力する言語を処理するための lexer というものと、 出力を行う formatter というものがいろいろ用意されている

今回は formatter として latex のみを考える

lexer は自分でいじる必要があるので、LaTeX 文書と同じディレクトリで管理したい

pygments にはそういうときのために、カレントディレクトリから lexer を探すという -x オプションがある

では minted で -x を指定するにはどうするかというと、普通は lexer の名前を書くところに mylexer.py -x というように書けばいいようだ (ひどい)

\begin{minted}[escapeinside=@@]{mylexer.py -x}
...
\end{minted}

なお、ここの escapeinside=@@ というオプションは、@...@ の内側は LaTeX として扱うという指定で、 mylexer.py が生成する中で @...@ という出力を使うためである

まぁ、毎回こう書くのはつらいので、\newminted と \newmintinline で shortcut を定義しておけばいいだろう (ファイルから読み込む \newmintedfile もやってもいいかもしれない)

\newminted[mycode]{mylexer.py -x}{escapeinside=@@}
\newmintinline[mycodeinline]{mylexer.py -x}{escapeinside=@@}

\begin{mycode}
...
\end{mycode}

\mycodeinline|...|

で、mylexer.py を用意する。 おそらく、ふつうは対象の言語の lexer をもとにカスタマイズするのだろうが、 今回は literate 相当のことをする説明のために、それだけを行うものとした

# -*- coding: utf-8 -*-

import re

from pygments.lexer import RegexLexer, words
from pygments.token import Text, Operator, Keyword

__all__ = [' CustomLexer ']

class  CustomLexer (RegexLexer):

    literate_keyword = {
        'forall' : '@\\ensuremath{\\forall}@'
    }

    literate_operator = {
        '->' : '@\\ensuremath{\\rightarrow}@',
        '=>' : '@\\ensuremath{\\Rightarrow}@',
    }

    def literate_keyword_callback(lexer, match):
        yield match.start(), Keyword, lexer.literate_keyword[match.group(0)]

    def literate_operator_callback(lexer, match):
        yield match.start(), Operator, lexer.literate_operator[match.group(0)]

    tokens = {
        'root': [
            (words(list(literate_keyword), prefix=r'\b', suffix=r'\b'),
             literate_keyword_callback),
            (r'(%s)' % '|'.join(list(literate_operator)[::-1]),
             literate_operator_callback),
            (r'.', Text),
        ],
    }

重要なのは、literate_keyword_callback (や literate_operator_callback) で、 ソースでマッチした match とは異なる文字列を yield している、というところである

ここでは、ソースの forall に対して @\ensuremath{\forall}@ を、-> に対して @\ensuremath{\rightarrow}@ を、=> に対して @\ensuremath{\Rightarrow}@ を、 生成している

(生成するところが数式モードでもそうでなくても、\ensuremath により \forall, \rightarrow, \Rightarrow は数式モードとして扱われる)

そうすると、LaTeX の出力では ∀ などと表示される

たとえば、以下の例を処理できる

t.tex:

\documentclass{article}
\usepackage{minted}

\newminted[mycode]{mylexer.py -x}{escapeinside=@@}
\newmintinline[mycodeinline]{mylexer.py -x}{escapeinside=@@}

\begin{document}
\begin{mycode}
nat -> nat.
forall (T:Type), list T -> nat.
\end{mycode}

foo \mycodeinline|nat -> nat, forall (T:Type), list T -> nat| bar
\end{document}

PDF生成:

pdflatex -shell-escape t.tex

[latest]


田中哲