【入門記事】 SATySFi のコマンドを定義する方法

公開:2020/12/16
更新:2020/12/30
12 min読了の目安(約11300字TECH技術記事

この記事は SATySFi advent calendar 2020 の10日目の記事です。
昨日は abenori さんによる記事「SATySFiで文書作ったときに思ったこととか」でした。
明日は na4zagin3 さんによる記事「Satyrographos Repo の CI の話」です。

monaqa です。
いつのまにか12月の中旬ですね。12月といったら大晦日、大晦日といったら SATySFi です。楽しく SATySFi でマークアップしましょう!

SATySFi でマークアップするには、コマンドを使いこなすことが必要不可欠です。逆に、コマンドさえ完璧に使いこなせればあなたはもうすでに立派な SATySFician、という見方もあります。そこで、アドベントカレンダーという折角の舞台を活かして「SATySFi のコマンドを使いこなす上でもしかすると役に立つかもしれない3つのこと」をまとめたいと思います。

  1. 【12/3】SATySFi の型について
  2. 【12/10】SATySFi のコマンド記法
  3. 【12/17】SATySFi のコマンドを定義する方法 (本記事!

ここで説明する内容は基本的に The SATySFibook に記載されています。詳しい説明はそちらを参照してください。ただし、この記事に関して誤りを発見された場合は記事下のコメントまたは Twitter の @mo_naqa などにご連絡いただけると助かります。
また、以下の解説は SATySFi v0.0.5 を想定しています。将来のバージョンで仕様が変わる可能性があるためご注意ください。

はじめに

第1回では SATySFi の型について、第2回ではコマンド記法について説明しました。今回は「自分でコマンドを定義する方法」を述べます。

SATySFi は、他のマークアップ言語と比較しても楽に「自身でマークアップを拡張する」ことができる言語だと思います。そのマークアップを拡張する礎になっているのが、本日説明するコマンド定義です。
コマンドを自身で定義できるようになれば、

  • 自分がよく書く内容・スタイルのショートカットを定義して楽に記述する
  • 既存のパッケージにない機能を追加する
  • 新たな記号等を定義する(特に数式において)

といったことができるようになります。非常に強力ですね。

本記事ではまずコマンドの定義をどこに書けばよいか述べた後、インラインコマンド・ブロックコマンド・数式コマンドの3種類のコマンドを定義する方法についてそれぞれ説明します。

コマンド定義はどこに書くか

SATySFi のコマンド定義はどこにでも書けるわけではありません。コマンドを定義できる場所は基本的に2箇所です。

  1. 文書ファイル (.saty 拡張子を持つファイル[1]) のプリアンブル
  2. パッケージファイル (.satyh 拡張子を持つファイル) の中

文書ファイルのプリアンブル

文書ファイルとは「組みたい文書の本文を記述するファイル」です。以下のような文書ファイルを考えてみましょう。

test.saty
@require: stdja
@require: code

StdJa.document (|
  ... 省略 ...
|) '<

  +p{
    Hello, \SATySFi;!
    Hello, \emph{world}!
  }
  +eqn(${
    \frac{\alpha}{2}
  });

>

この StdJa.document よりも手前の箇所を、LaTeX に倣いプリアンブルと呼びます(便宜上そう呼ぶこととしています)。@require: からはじまる行は SATySFi のパッケージファイルを読み込む文であり、これらはファイルの最初に書かれなければなりません。コマンド定義は @require: の後のプリアンブル部分に書きます。たとえば、上の例であれば以下のように書きます。

test.saty
@require: stdja
@require: code

% ここからコマンド定義開始
let-inline \hoge = {hoge}
let-block +fuga = '<>

in % コマンドの定義を行う場合は、プリアンブルの最後に `in` をつける

StdJa.document (|
  ... 省略 ...
|) '<

  +p{
    Hello, \SATySFi;!
    Hello, \emph{world}!
  }
  +eqn(${
    \frac{\alpha}{2}
  });

  +fuga;  % プリアンブルで定義したコマンドを使えるようになる
>

この方法は比較的お手軽ですが、複数の文書ファイルに定義を使い回すことができません。

パッケージファイルの中

パッケージファイルはコマンドや関数などの定義をまとめておくためのファイルです。パッケージファイルには .satyh という拡張子をつけます[2]。たとえば local.satyh という名前のファイルを作成し、そこに以下の内容を書き込みます。

local.satyh
let-inline \hoge = {hoge}
let-block +fuga = '<>

そして、同じディレクトリに置いた test.saty@import: local を冒頭に書くことで、文書ファイルでコマンドを使えるようになります。

test.saty
@require: stdja
@require: code
@import: local

StdJa.document (|
  ... 省略 ...
|) '<

  +p{
    Hello, \SATySFi;!
    Hello, \emph{world}!
  }
  +eqn(${
    \frac{\alpha}{2}
  });

  +fuga;  % local ファイルの中で定義したコマンドを使えるようになる
>

パッケージファイルという新たなファイルを作成する必要があるものの、複数の文書ファイルで取り回して使うことができます。なお @import: には文書ファイルからの相対パスを与えることができるため、同じ階層にない場合でも対応可能です。

インラインコマンドの定義

インラインコマンドを定義するには、いかなる場合でも let-inline というプリミティヴを用います。たとえば、 NN 個の引数を受け取るコマンド \cmd-name を定義する場合は以下のように記述します。

let-inline ctx \cmd-name arg1 arg2 ... argN = «expr»

let-inline の後ろには以下のような要素が並びます。

  • ctx: 第0引数context 型を持ち、「コマンドが呼び出される際のテキスト処理文脈」を表します。変数名に指定はありませんが、慣習的に用いられている ctx を使っておけば良いでしょう。
  • \cmd-name: 定義したいコマンドの名前。
  • arg1, ..., argN: コマンド引数。順に羅列します。
  • «expr»: 定義の中身を表す式。 inline-boxes 型の式を入れます。

また、以下のような記法もあります。

let-inline \cmd-name arg1 arg2 ... argN = «expr»
  • \cmd-name: 定義したいコマンドの名前。
  • arg1, ..., argN: コマンド引数。順に羅列します。
  • «expr»: 定義の中身を表す式。こちらの書き方では、 inline-text 型の式を入れます。

1番目の記法との違いは、「テキスト処理文脈を表す第0引数が省略されていること」と、「返り値がインラインテキスト型であること」の2点です。テキスト処理文脈を用いなくとも定義できるような、単純なコマンドを定義したいときにはこちらを使うのが良いでしょう。

一般的な形式だけ見ても使い方がよくわからないと思うので、いくつか具体例を紹介しましょう。

例1: パングラムを表示する

まずは最も単純な例を。 "The quick brown fox jumps over the lazy dog." というテキストを表示するコマンド \pangram; を定義してみます。これは以下のように定義できます。

let-inline ctx \pangram =
  read-inline ctx {The quick brown fox jumps over the lazy dog.}

read-inline プリミティヴを用いて {The quick brown fox jumps over the lazy dog.} というインラインテキストをインラインボックス列に変換しています。これは更に、以下のように簡潔に書くことができます。

let-inline \pangram = {The quick brown fox jumps over the lazy dog.}

非常に簡潔ですね。 \pangram;[] inline-cmd 型を持ち(最初の [] は引数がなにもないことを表す)、以下のように使います。

+p{これは英語のパングラムです:\pangram;}

例2: テキストのフォントサイズを2倍にする

続いては、インラインテキストを引数にとり、そのテキストのフォントサイズを2倍にして表示するコマンド \double{«text»} を定義してみます。これは以下のように定義できます。

let-inline ctx \double it =
  let fsize = get-font-size ctx in
  let new-ctx = ctx |> set-font-size (fsize *' 2.) in
  read-inline new-ctx it

少しプログラミング感が増してきました。まだそこまで複雑なことはしていませんが、何をやっているのか簡単にさらってみましょう。

  • 最初に get-font-size プリミティヴを用いて、現在のテキスト処理文脈 (ctx) で設定されているフォントサイズを取得します。 fsizelength 型を持つ値となります。
  • 続いて set-font-size プリミティヴを用いて、「フォントサイズを fsize の2倍にする」ような新たなテキスト処理文脈を取得します。 new-ctxcontext 型を持ちます。
  • 新たな new-ctx を用いてインラインテキスト it を組みます。

|> はパイプライン演算子と呼ばれ、x |> f は基本的に f x (関数適用)と同じ働きをします。従って上の例の3行目は let new-ctx = set-font-size (fsize *' 2.) ctx in と書いてもかまいません。
パイプライン演算子は h (g (f x)) のように、複数の関数を立て続けに適用したい際に x |> f |> g |> h と分かりやすく書ける利点があります。テキスト処理文脈は特に関数を立て続けに適用する機会が多いため、この演算子が重宝します。

% フォントサイズ、テキストの色、段落間の余白をいっぺんに設定する例
let new-ctx =
  ctx |> set-font-size fsize       % フォントサイズを fsize に設定
      |> set-text-color Color.red  % テキストの色を赤色に設定
      |> set-paragraph-margin (fsize *' 1.2) (fsize *' 1.2)
                                   % 段落間の余白を fsize の 1.2 倍に設定
in

出来上がった \double コマンドは、 [inline-text] inline-cmd 型を持ちます。

例3: テキストのフォントサイズを r 倍にする

先程の例に、さらに引数を増やしてみましょう。テキストのフォントサイズをいじる際、拡大率をユーザが指定できるようにします。\enlarge(«ratio»){«text»} とすることで、textをratio倍に拡大(縮小)するコマンドを定義してみます。引数が1つ増えるだけで、さほど難しくありません。

let-inline ctx \enlarge ratio it =
  let fsize = get-font-size ctx in
  let new-ctx = ctx |> set-font-size (fsize *' ratio) in
  read-inline new-ctx it

出来上がった \enlarge コマンドは [float; inline-text] inline-cmd 型を持ちます。

例4: フォントサイズを r 倍にしたパングラムを表示する

続いて、フォントサイズを rr 倍にしたパングラムを表示するコマンドを定義してみましょう。今までの実装を組み合わせればよいですね。

let-inline ctx \show-enlarged-pangram ratio =
  let fsize = get-font-size ctx in
  let new-ctx = ctx |> set-font-size (fsize *' ratio) in
  read-inline new-ctx {The quick brown fox jumps over the lazy dog.}

出来上がった \show-enlarged-pangram コマンドは [float] inline-cmd 型を持ちます。

ところで、このコマンドはこれまでに作成した \enlarge\pangram コマンドの組み合わせでも実現できます。すでに定義したコマンドを活かせないでしょうか?もちろん可能です。

let-inline \show-enlarged-pangram ratio = {\enlarge(ratio){\pangram;}}

この書き方なら第0引数の ctx を省略して簡潔に書くことができますし、 \enlarge\pangram の中身(実装)が分からなかったとしても問題ありません。

例5: テキストのフォントサイズを r 倍(デフォルトは2倍)にする

「フォントサイズを2倍にすることが多いが、たまに1.5倍や3倍にすることもある」というユースケースを考えてみましょう。この場合はフォントサイズの拡大率をオプション引数にすると便利です。先程の \enlarge コマンドの第一引数をオプション引数にしてみましょう。

let-inline ctx \enlarge ?:ratioopt it =
  let ratio = match ratioopt with
    | Some(r) -> r
    | None    -> 2.0
  in
  let fsize = get-font-size ctx in
  let new-ctx = ctx |> set-font-size (fsize *' ratio) in
  read-inline new-ctx it

オプション引数は、引数の名前の冒頭に ?: を付けて表します。 ratioopt はこの場合 float option 型を取り、 match 式を用いて float 型に unwrap することができます。

標準で用意されている option パッケージを用いれば、「ratiooptSome(r) のときは ratior に、 None のときはデフォルト値 2.0 を r に設定する」コードを楽に書くことができます。

let ratio = ratioopt |> Option.from 2.0 in

ブロックコマンドの定義

ブロックコマンドを定義するには、いかなる場合でも let-block というプリミティヴを用います。たとえば、 NN 個の引数を受け取るコマンド +cmd-name を定義する場合は以下のように記述します。

let-block ctx +cmd-name arg1 arg2 ... argN = «expr»

let-block の後ろには以下のような要素が並びます。

  • ctx: 第0引数context 型を持ち、「コマンドが呼び出される際のテキスト処理文脈」を表します。変数名に指定はありませんが、慣習的に用いられている ctx を使っておけば良いでしょう。
  • +cmd-name: 定義したいコマンドの名前。
  • arg1, ..., argN: コマンド引数。順に羅列します。
  • «expr»: 定義の中身を表す式。 block-boxes 型の式を入れます。

…はい、大部分が let-inline の説明のコピペです。実際、 let-block プリミティヴの使い方は、型が inline-textinline-boxes から block-textblock-boxes に変わる点を除いて let-inline とさほど変わりません。

let-inline と同様に以下のような記法もあります。

let-block +cmd-name arg1 arg2 ... argN = «expr»
  • +cmd-name: 定義したいコマンドの名前。
  • arg1, ..., argN: コマンド引数。順に羅列します。
  • «expr»: 定義の中身を表す式。こちらの書き方では、 block-text 型の式を入れます。

こちらも1つ具体例を紹介します。

例6: 段落を作成する (+p)

+p はこれまでの説明でも何度か登場したとおり、段落を作成するコマンドです。単純に段落を作成する機能だけ持たせたいのであれば、以下のように定義することができます。

let-block ctx +p inner =
  let ib-inner = read-inline ctx inner in
  let quad-indent = inline-skip (get-font-size ctx) in
  let br-parag = quad-indent ++ ib-inner ++ inline-fil in
  line-break true true ctx br-parag
  • まず、inline-text 型を持つ第1引数の innerinline-boxes 型に変換する。
    その際に第0引数の ctx を用いる。
  • 四分空きの長さを持つインデントに相当するインラインボックス列 quad-indent を作成する。
  • ib-inner の冒頭に quad-indent を、末尾に「インライン方向に無限に伸びるグルー」(inline-fil) を挿入する。
  • 作成したインラインボックス列をもとに行分割を行う。

出来上がった +p[inline-text] block-cmd 型を持ちます。

なお、 stdja 等のクラスファイルでは「章や節冒頭の段落ではインデントを付けない」という仕様となっており、もう少し複雑です。

数式コマンドの定義

数式コマンドを定義するには、いかなる場合でも let-math というプリミティヴを用います。

let-math \cmd-name arg1 arg2 ... argN = «expr»

let-math の後ろには以下のような要素が並びます。

  • \cmd-name: 定義したいコマンドの名前。
  • arg1, ..., argN: コマンド引数。順に羅列します。
  • «expr»: 定義の中身を表す式。 math 型の式を入れます。

数式コマンドでは、 let-inlinelet-block のときと異なり、第0引数としてテキスト処理文脈を指定することができません(数式コマンドでもテキスト処理文脈が欲しくなる場面が少なくないので、個人的にはあると便利だなあと思っています)。

例7: マクローリン展開の式を楽に書く

あまり良い具体例が思いつかなかったので。\exp-coef{n}exp(x)\exp(x) をマクローリン展開したときの第nn項の式を与える、とかやってみましょうか。
こんな感じになります。

let-math \exp-coef n = ${\frac{x^{#n}}{#n\!}}

はい、終わりです。これで [math] math-cmd 型を持つコマンド \exp-coef を定義したことになります。単純ですね。nmath 型の変数であり、数式リテラル ${ ... } の中では #n とすることで参照することができます。

定義した \exp-coef はこんなふうに使うことができます。

+math(${
  e^x = \sum_{n=0}^\infty \exp-coef{n}
      = 1 + x + \exp-coef{2} + \exp-coef{3} + \exp-coef{4} + \cdots
});

簡単ですね!

ちなみに… let-math だけは式の前に置くことができます。つまり、わざわざプリアンブルやヘッダファイルで定義しなくとも

+math(
  let-math \exp-coef n = ${\frac{x^{#n}}{#n\!}} in
  ${
    e^x = \sum_{n=0}^\infty \exp-coef{n}
        = 1 + x + \exp-coef{2} + \exp-coef{3} + \exp-coef{4} + \cdots
  }
);

のようにして、 \exp-coef コマンドを使うことができます(こちらは let-math による定義の後に in が必要)。この場合はローカルなコマンドとして扱われ、定義を行った +math コマンドの外では \exp-coef コマンドが無効となります。使い捨てのショートカットを作成したいときに便利ですね。

最後に

最後まで読んでくださりありがとうございました。12/3, 12/10, そして本日と、3日に分けて SATySFi のコマンド周辺を整理してみました。

SATySFi はまだ新しい組版用言語ですから、TeX/LaTeX などの古くから存在する言語と比較するとお世辞にもまだ機能が豊富とは言えません。私自身 AZmath など汎用的に使えそうなパッケージ開発を行っているものの、本格的な文書を作成しようとすると、まだ各々のユーザが各々の需要に合わせてカスタマイズしているのが現状だと思います。この記事は、そんなカスタマイズのための入り口というつもりで書きました。

まだ SATySFi を触ったことのない人が、少しでも「SATySFiなら自分にも書けそう」と思ってもらえたら幸いです。

脚注
  1. 文書ファイルについては、拡張子が .saty であることは MUST ではないものの SHOULD ではある、という立場のようです。つまり .saty でなくとも仕様上コンパイルは通るが、 .saty にすることが強く推奨されているという状況ですね。 ↩︎

  2. .satyg という拡張子を用いる場合もありますが、あくまで PDF 出力を目的とするパッケージファイルを作成するのであれば、ひとまず .satyh という拡張子を使うのが良いでしょう。 ↩︎