ClojureからTeXを作成する
ClojureからTeXドキュメントを作成するためのライブラリーを作成したので、紹介したいと思います。
quick start
依存関係に以下を追加:
[texdata "0.1.6-SNAPSHOT"]
前提として、TeXがシステムにインストールされている必要があります。Leiningenユーザーであれば、プロジェクト内の適当なディレクトリにtest.tex等のファイルを作り、以下コードを評価すれば結果を閲覧できるかと思います。
(ns texdata-demo.core
(:require [texdata.core :refer :all]))
(def test-path "test/texdata_demo/test.tex")
(compile-and-view
test-path
(tex [:documentclass "article"]
[:document "hello world"]))
使い方
本ライブラリーはClojureのデータ(ベクトルなど)をTeXが理解可能な文字列に変換することを主な目的としています。その機能を担っているのが、
tex, tex->, tex->>
等の関数です。以下、上記関数の使い方を解説していきます。
簡単な例
(tex 1)
;; => "1"
(tex "a")
;; => "a"
(tex :sin)
;; => "\\sin"
(tex [:frac 1 2])
;; => "\\frac{1}{2}"
(tex [:equation [:frac 1 2]])
;; => "\\begin{equation} \\frac{1}{2} \\end{equation}"
上記のように、texは引数の型によって異なる文字列変換を行います。例えば、
deta type | rule | code | output |
---|---|---|---|
文字列 | そのまま | (tex "a) | "a" |
数字 | そのまま文字列に変換 | (tex 1) | "1" |
キーワード | 独立したTeX命令 | (tex :sin) | "\sin" |
ベクトル | 引数をとるTeX命令 | (tex :frac 1 2) | "\frac{1}{2} |
また、texはベクトルでまとめられたデータの先頭要素がTeX命令を表すものでない場合、そのベクトルを一つのまとまりとして文字列に変換します。例えば、以下のようにするとネストした分数を表現できます。
(tex [:frac 1 [:frac 2 3]])
;; => "\\frac{1}{\\frac{2}{3}}"
数式
インライン数式モード
TeXの数式モードにはインライン数式モードとディスプレイ数式モードがあります。インライン数式モードに対応するコマンドは本ライブラリーでは、
:dol
としています。
(tex "Let's suppose that " [:dol "n"] "is an even number.")
;; => "Let's suppose that $ n $ is an even number."
ディスプレイ数式モード
番号付き数式には:equation, 番号なし数式には:mathを使います。
(tex [:math "E=mc"])
;; => "\\[ E=mc \\]"
(tex [:equation "E=mc"])
;; => "\\begin{equation}E=mc \\end{equation}"
指数、添字
指数と添字をつけるには、以下のように、修飾したいデータの次に:sub,:superという特別なキーワードを使います。
(tex ["a" :super 2 :sub 2])
;; => "a_{2}^{2}"
(tex [:int :sub 0 :super 1 "f(x)dx"])
;; => "\\int_{0}^{1}f(x)dx"
引数をとる命令
fracのように、引数をとる命令は、基本的に
[keyword arg1 arg2...]
という形のベクトルで表されます。ここで、先頭のキーワードは:fracのように、その命令に対応するものである必要があります。上に登場している:fracや:equationはこのような意味をもつキーワードということです。
TeXには非常に多くの命令がありますが、本ライブラリーではTeX命令の多くをそのまま対応するキーワードとして認識するようにしています。例えば、行列を表記するための命令として、
matrix, pmatrix, vmatrix
などがありますが、こうした命令をそのままキーワードにしたものが利用可能です:
(tex [:matrix 1 :amp 2 :next 3 :amp 4])
;; => "\\begin{matrix}1 & 2 \\\\ 3 & 4 \\end{matrix}"
(tex [:vmatrix 1 :amp 2 :next 3 :amp 4])
;; => "\\begin{vmatrix}1 & 2 \\\\ 3 & 4 \\end{vmatrix}"
オプショナルな引数
TeXの命令の中には、オプショナルな引数をとるものがあります。例として、
\documentclass
が挙げられます。この命令は文書クラスを指定するためのものですが、
\documentclass{article}
のようにオプションなしで使うこともでき、あるいは
\documentclass[12pt,a4paper,oneside,draft]{report}
のようにいくつかのオプションを指定することも可能です。
このような命令にオプショナルな引数を渡すために、本ライブラリーでは、以下のようにマップを挿入する仕様になっています。
(tex [:documentclass "article"])
;; => "\\documentclass{article}"
(tex [:documentclass
{:opt ["12pt" "a4paper" "oneside" "draft"]}
"report"])
;; => "\\documentclass[12pt,a4paper,oneside,draft]{report}"
例の検索
これまで見てきたような各種のコマンドは、Clojure関数ではないため、ドキュメントが用意されていません。しかし、
(example command-key)
によって、各コマンドに対応する例を得ることが可能です。(残念ながら今のところ全てのコマンドに例があるわけではありませんが。)
(example :color)
;; => [:color :red "red text"]
また、
(get-all-commands)
を評価することで、全てのコマンドを参照できます。
アロー関数
Clojureに慣れている方であれば、
clojure.core/->, clojure.core/->>
などの「アローマクロ」を頻繁に活用しているのではないでしょうか。Lispに対する古くからある不満として、関数をつなぎ合わせると大量の括弧が出てきて可読性が損なわれる、というものがありますが、この問題はアローマクロを使うことである程度解消されます。
本ライブラリーでも、clojure.core/->, clojure.core/->>と同様の機能を持つ関数
tex->, tex->>
を提供しています。
clojure.core/-> と同様に、tex->は前の結果を次のコマンドの初めの引数として挿入します。例えば、
(tex [:huge [:equation "E=mc"]])
と同じ結果を得るために、
(tex-> "E=mc" :equation :huge)
;; => "\\begin{huge}\\begin{equation}E=mc \\end{equation} \\end{huge}"
と書いてもいい、ということです。
tex->>についても同様ですが、tex->>では前の結果が次のコマンドの最後の引数として挿入されます。tex->>の使用例として
:color
というコマンドを取り上げてみます。:colorは2つ以上の引数をとり、一つ目の引数が後に続くものの色を指定します:
(tex [:color "red" "red text"])
;; => "\\color{red}{red text}"
ゆえに、先程と同様のことをするのであれば、tex->>を使い直前の結果を:colorの最後の引数として挿入する必要があります。
(tex [:color "red" [:math "E=mc"]])
;; => "\\color{red}{\\[ E=mc \\]}"
(tex->> [:math "E=mc"] [:color "red"])
;; => "\\color{red}{\\[ E=mc \\]}"
命令の追加
defcmdは新しいコマンドを追加するためのマクロです。ここでは、新しい命令の追加方法を解説していきます。
本ライブラリーでは全てのコマンドを3種類に分類しています。
種類 | 引数をとるか | \begin{...}...\end{...} | 例 |
---|---|---|---|
environment | YES | YES | :equation, :document |
normal | YES | NO | :frac, :text, :color |
independent | NO | NO | :amp |
新規にコマンドを追加する際は、そのコマンドがどの種類に当たるのかを指定する必要があります。
例として、
:hoge
というコマンドを追加したいとします。さらに、このコマンドは:environmentタイプ、すなわち
\begin{hoge}...\end{hoge}
のように、\beginと\endで囲まれるタイプのものだとしましょう。このような場合、
(defcmd :hoge :environment :default)
とすれば、:hogeが追加されます。
(tex [:hoge 1])
;; => "\\begin{hoge}1 \\end{hoge}"
上の例で見たように、defcmdは
(defcmd command-key command-type & body)
という文法を持っています。TeXの多くの命令は、:equationのように\begin,\endで中身を囲むだけですが、そのような命令であれば上の:hogeのようにキーワード:defaultを使用することで登録できます。
特別な実装が必要な場合
追加したい命令が個別の実装を必要とする場合には、然るべくデータから文字列の変換を規定しなくてはなりません。
例として、
:mycolor
というコマンドを新たに登録するとし、このコマンドは
(tex [:mycolor "red" ....])
のように、第一引数で「色」をとるものとしましょう。そして、上のコードを評価した結果として、
"mycolor{red}{...}"
という文字列を得たいとします。このような場合、以下のように、defcmdで文字列変換を実装します。
(defcmd :mycolor :normal [[_ color & more]]
(format "\\mycolor{%s}{%s}" color (tex more)))
テスト:
(tex [:mycolor "red" 1])
;; => "\\mycolor{red}{1}"
このように個別にコマンドの実装をする場合、defcmdの&body部分は通常の関数定義と同様の形になります。その際、引数として常に、texに渡せるベクトルを入れることに注意します。ゆえに、そのベクトルの初めの要素は常に新たに追加しているコマンドのキーワード(上の例では:mycolor)ということになります。
少し長い例
最後に、少し長い例を紹介しておきます。
(def dirac-delta
(tex
[:documentclass "article"]
[:usepackage "amsmath"]
[:usepackage "amssymb"]
[:usepackage {:opt ["left = 20mm" "right = 20mm"] } "geometry"]
[:document
["The Dirac delta function" [:dol :delta "(x)"] "satisfies:"
[:enumerate
[:item
[:math :delta "(x)" :eq 0 :sp
[:text "for all" [:dol "x" :neq 0]]]]
[:item
[:math :int :sub ["-" :infty] :super :infty
:delta "(x)dx"
:eq 1]]]
"From these properties, it follows that for all function"
[:dol "f,"]
[:math
:int :sub ["-" :infty] :super :infty
:delta "(x)" "f(x)" "dx"
:eq
"f(0)."]]]))
(compile-and-view
"test/texdata/examples/out/test.tex"
dirac-delta)