これは「SATySFi Advent Calendar 2019」の10日目の記事です。
9日目はbd_gfngfnさんでした。11日目はamaotoさんです。
この記事に出てくるクラスファイルとパッケージはGitHubのリポジトリに置いてあります。
自由に覗いていってください
はじめに
SATySFiにはPDFファイルを出力する機能だけでなく、テキストファイルを出力する機能もあります。
ファイル読み込み・字句解析・構文解析・型検査・ファイル書き出しとそれらのエラー報告をSATySFi側がやってくれるので、パッケージ作成者はデータをどう変換すればよいかだけを考えれば良いのでとても便利です。
さっそく使ってみる
さて、グダグダいう前に早速使ってみたいと思いますが、さすがにいくつか事前知識が必要なのでまずはそれについて説明します。
起動
$ satysfi --text-mode 〈形式名称〉 〈入力ファイル名〉 -o 〈出力ファイル名〉
という形でSATySFiのテキスト出力モードが起動します。
拡張子の自動判定ができないので出力ファイル名は省略できません。
形式名称は変換先のテキストファイルの形式を指定するもので、"html"
や"platex,latex"
のようにカンマ区切りで入力します。この時、より限定的な形式を先に書くようにします。
例として、test.saty
ファイルを処理してtest.txt
というテキストファイルを出力するようにしたいときは
$ satysfi --text-mode "text" test.saty -o test.txt
のように指定してSATySFiを起動します。
拡張子
文書ファイルの拡張子は.saty
で共通です。しかし、パッケージファイルの拡張子はやや特殊です。
まず、PDF出力モードで使用している.satyh
が拡張子のパッケージファイルは一切使えません。
「PDF出力モードとテキスト出力モードの両方で使えるパッケージ」です。こうしたパッケージの拡張子は.satyg
になります。例としてはリスト構造を扱うlist.satyg
やオプションを扱うoption.satyg
などです。
次にテキスト出力モードでしか使えないパッケージです。
これらは.satyh-〈形式名称〉
という形をしています。
起動時に指定した形式名称がここで生きてくるわけですね。
形式名称が単体であった場合(例えば"html"
であったとき)は@require: hoge
と文書の冒頭に指定してあったらhoge.satyh-html
というファイルとhoge.satyg
というファイルが探される、ということになります。
形式名称が複数指定してあった場合は最初に指定してあったものから探され、最後に.satyg
が来ます。
なので、@require: fuga
としてあって起動時に指定した形式名称が"platex,latex"
であったときは
fuga.satyh-platex
fuga.satyh-latex
fuga.satyg
の順番で探されることになるというわけです。
実行
さて、まずは実際に使ってみることにしましょう。
テキスト出力モードで使えるクラスファイルはほとんどないのでここで一つ、作ってしまいましょう。
まずはコピペで良いので以下の2つのファイルを作りましょう。
text-cls.satyh-text
let tinfo-base = get-initial-text-info ()
let document bt = stringify-block tinfo-base bt
let-block tinfo +p it =
let lf = break tinfo in
let main = stringify-inline tinfo it in
main ^ lf
let-inline tinfo \SATySFi = `SATySFi`
test.saty
@import: text-cls
document '<
+p{\SATySFi;のテキスト出力モードです。}
+p{頑張ればHTMLファイルだって作れそうです。}
>
これらができたら
$ satysfi --text-mode "text" test.saty -o test.txt
をしてtext.txt
ファイルを作ってみましょう。
テキスト出力モードでのコマンドの作り方
text-cls.satyh-text
というクラスファイルをさきほど作りましたが、PDF出力モードでのそれとは雰囲気が少し違うと思います。
ここでは、テキスト出力モードでのコマンドの作り方をざっくりと解説していきます。
使えるプリミティブと使えないプリミティブ
テキスト出力モードとPDF出力モードでは使えるプリミティブが異なり、扱える型も異なります。
まず、context型・inline-boxes型・block-boxes型に関わるプリミティブは使えず、したがってcontext型・inline-boxes型・block-boxes型も実質的に扱えません(新しい型を作るときにcontext型などを使ってもエラーはでません。扱うプリミティブが無いだけで)。
その他の文字列操作や計算系のプリミティブは使えます。
数式に関してはプリミティブを使うことはできますが、math型をstring型などにする機構がまだ実装されていないので実質的に使えません。
詳しくはvminstdef.yamlでの実装を見てis-text-mode-primitive
がyes
になっているかを確認してください。
特別に使えるプリミティブ
PDF出力モードでしか使えないプリミティブがあったように、テキスト出力モードでしか使えない型やプリミティブも存在します。
数は少ないですが、以下の通りです。
-
text-info
型 get-initial-text-info : unit -> text-info
deepen-indent : int -> text-info -> text-info
break : text-info -> string
stringify-inline : text-info -> inline-text -> string
stringify-block : text-info -> block-text -> string
それぞれ説明していきます。
まず、テキスト出力モードでのcontext型にあたるtext-info型です。
この中にはインデント量などの情報が含まれています。
get-initial-text-info
はtext-info型を生成することができる唯一のプリミティブで、主にクラスファイルの作成の時に使用します。
deepen-indent
を使うと、インデント量を指定した数字の分だけ増やすことができます。ただし、負の値を指定してもインデント量は減りません。
break
は改行文字を生成し、text-infoに基づいてインデントを直後に置きます。
stringify-inline
はインラインテキストを出力文字列に変換するプリミティブで、PDF出力モードでのread-inline
を使う感覚に近いです。
stringify-block
はstrinify-inline
のブロックテキスト版です。
コマンドの作り方
PDF出力モードと同様にlet-inline
とlet-block
を用いますし、第0引数の扱いも同じです。
第0引数を取るときはstring型を、取らない時はinline-text型かblock-text型を返すことが必要です。
試しに、MarkDownでのハイパーリンクを出力する\href
コマンドを実装してみるとこのようになります。
let-inline tinfo \href url it =
let text = stringify-inline tinfo it in
`[` ^ text ^ `]` ^ `(` ^ url ^ `)`
そして、使ってみるとこのように変換されるでしょう。
\href(`https://github.com/gfngfn/SATySFi`){SATySFi}
[SATySFi](https://github.com/gfngfn/SATySFi)
クラスファイルとパッケージを作ってみる
SATySFiのテキスト出力モードに慣れるためにsaty
ファイルに変換するためのクラスファイルとパッケージを実際に作ってみましょう。
クラファイルの名前はstdja.satyh-satysfi
とし、stdjaクラスファイルで定義されているコマンドが全て使えた上で出力したコードがSATySFiできちんとコンパイルできることを目標とします。
基本的な関数を実装する
まずは基本的な値を文字列に置き換える関数を用意します。
これは他のパッケージにも使いまわせるのでsatysfi-base.satyh-satysfi
というファイルのSATySFiBase
というモジュールの中に実装しておきましょう。
module SATySFiBase :sig
val to-tuple : string list -> string
val from-inline-text : text-info -> inline-text -> string
val from-block-text : text-info -> block-text -> string
val from-block-text-pro : text-info -> block-text -> string
val from-string : text-info -> string -> string
val from-bool : text-info -> bool -> string
val from-length : text-info -> length -> string
val from-color : text-info -> color -> string
val add-paren : string -> string
val concat : string list -> string
val make-inline-cmd : text-info -> string -> string list -> string
val make-block-cmd : text-info -> string -> string list -> string
end = struct
let to-tuple lst =
let join n s1 s2 =
if n == 0 then
s2
else
s1 ^ `, `# ^ s2
in
List.fold-lefti join ` `
let from-inline-text tinfo it =
`{` ^ stringify-inline tinfo it ^ `}`
let from-block-text tinfo bt =
`<` ^ stringify-block tinfo bt ^ `>`
let from-block-text-pro tinfo bt =
`'<` ^ stringify-block tinfo bt ^ `>`
let from-string tinfo str =
let-mutable count <- 0 in
let-rec make-str initial n str =
if n <= 0 then
str
else
make-str initial (n - 1) (str ^ initial)
in
let-rec sub str num =
if (string-length str) <= 0 then
!count
else
let str-len = string-length str in
let str-head = string-sub str 0 1 in
let str-tail = string-sub str 1 (str-len - 1) in
if string-same (`` ` ``) str-head then
sub str-tail (num + 1)
else
if num > !count then
let () = count <- num in
sub str-tail 0
else
sub str-tail 0
in
let back-quote =
let n = (sub str 0) + 1 in
make-str (`` ` ``) n (` `)
in
back-quote ^ #` `# ^ str ^ #` `# ^ back-quote
let from-int tinfo n =
arabic n
let from-float tinfo fl =
show-float fl
let from-bool tinfo b =
if b then
`true`
else
`false`
let from-length tinfo len =
show-float (len /' 1pt) ^ `pt`
let from-color tinfo color =
let to-tuple-f f lst =
List.map f lst |> to-tuple
in
match color with
| Gray(f) -> `Gray(` ^ show-float f ^ `)`
| RGB(r, g, b) -> `RGB(` ^ to-tuple-f show-float [r;g;b] ^ `)`
| CMYK(c, m, y, k) -> `CMYK(` ^ to-tuple-f show-float [c;m;y;k] ^ `)`
let from-inline-text tinfo it =
`{` ^ stringify-inline tinfo it ^ `}`
let from-block-text tinfo bt =
`<` ^ stringify-block tinfo bt ^ `>`
let from-block-text-pro tinfo bt =
`'<` ^ stringify-block tinfo bt ^ `>`
end
from-string
は文字列の中に連続ででてくるバッククオーテーションの最高数を取得しないといけないので実装がやや複雑になっていますが、あまり難しくないと思います(一文字ずつ読んでカウントしているだけですね)。
inline-text
やblock-text
をstring
にするためにtext-info
が必要なことに注意しましょう。
次に値を括弧でくくるためのadd-paren
関数とstring list
を繋げるためのconcat
関数、そしてコマンドを定義しやすくするmake-inline-cmd
関数とmake-block-cmd
を実装します。
これも実装を見ればすぐにわかると思います。
良く使いそうなのでこれもSATySFiBase
に追加しておきましょう。
let add-paren s = `(` ^ s ^ `)`
let concat = List.fold-left (^) ` `
let make-inline-cmd tinfo name lst =
let info = deepen-indent 2 tinfo in
let str-lst = List.map add-paren lst in
break info ^ `\` ^ name ^ concat str-lst ^ `;` ^ break info
let make-block-cmd tinfo name lst =
let info = deepen-indent 2 tinfo in
let str-lst = List.map add-paren lst in
break info ^ `+` ^ name ^ concat str-lst ^ `;` ^ break info
document関数を定義する
元にしているstdja.satyh
の引数に揃えますが、今回は時間の都合でレコードと本文を与えるだけにします。
型は
document : 'a -> block-text -> string
constraint 'a :: (|
title : inline-text;
author : inline-text;
show-toc : bool;
show-title : bool;
package : string list;
|)
となり、あまり変わりませんが、最終的に返す型がdocument
型ではなくstring
型になっているということに注意しましょう。また、使用するパッケージを文字列で与えさせるようにしています。
実装は単純で、レコードの中身を使ってレコードを再構築し、本文と組み合わせるだけです。
組み合わせる際にSATySFiBase
に実装したconcat
関数を用います。
また、使用パッケージは@require:
や@import:
の部分まで含めて執筆者が書く必要がありますが、satysfi-base.satyh-satysfi
にそれようの関数を用意しておいたので大丈夫でしょう。
let document record inner =
let tinfo = get-initial-text-info () in
let title = record#title in
let author = record#author in
let show-title = record#show-toc in
let show-toc = record#show-title in
let package-lst = record#package in
let head = `@require: stdja` in
let package =
List.fold-left (fun s1 s2 -> s1 ^ break tinfo ^ s2) ` ` package-lst
in
let set =
concat [
`document (|` ^ break tinfo;
#` title = `# ^ from-inline-text tinfo title ^ `;` ^ break tinfo;
#` author = `# ^ from-inline-text tinfo author ^ `;` ^ break tinfo;
#` show-title = `# ^ from-bool tinfo show-title ^ `;` ^ break tinfo;
#` show-toc = `# ^ from-bool tinfo show-toc ^ `;` ^ break tinfo;
`|)`;
]
in
let main = from-block-text-pro tinfo inner in
concat [
head;
package;
break tinfo;
set;
main;
]
コマンドの実装
コマンドはSATySFiBase
に実装したmake-inline-cmd
などの関数を用いればすぐにできそうです。
例えば、+p
コマンドを定義しようとしたら
let-block tinfo +p it = make-block-cmd tinfo `p` [from-inline-text tinfo it]
とするだけで良いんですね。簡単です。
パッケージを実装してみる。
さて、ここでパッケージを作ってみましょう。
試しに作るのは\href
コマンドを提供するannotパッケージです。
この\href
コマンドは[string; inline-text]
を引数に取るので、以下のように実装できますね(本当は囲い枠の設定ができるのですが複雑になるので今回は無かったことにします……)。
なんと簡単なのでしょう!
@import: satysfi-base
module Annot :sig
direct \href : [string; inline-text] inline-cmd
end = struct
open SATySFiBase
let-inline tinfo \href url inner =
make-inline-cmd tinfo `href` [from-string tinfo url; from-inline-text tinfo inner]
end
使ってみる
@import: stdja
@import: annot
document (|
title = {テキスト出力モード};
author = {puripuri2100};
show-title = true;
show-toc = false;
package = [require `annot`];
|) '<
+p{\href(`https://github.com/gfngfn/SATySFi`){SATySFi}のリポジトリです!}
+p{こっちは\href(`https://adventar.org/calendars/3929`){SATySFi Advent Calendar 2019}}
>
という文書を書いて$ satysfi --text-mode "satysfi" test.saty -o test-out.saty
で変換してみましょう。
面白いですね
これを機に皆さんもSATySFiのテキスト出力モードに手を出してみましょう。