SATySFiのテキスト出力モードで遊ぼう！

これは「[SATySFi Advent Calendar 2019](https://adventar.org/calendars/3929)」の10日目の記事です。

9日目は[bd_gfngfnさん]()でした。11日目は[amaotoさん]()です。


この記事に出てくるクラスファイルとパッケージは[GitHubのリポジトリ](https://github.com/puripuri2100/saty-to-saty)に置いてあります。
自由に覗いていってください

# はじめに

SATySFiにはPDFファイルを出力する機能だけでなく、テキストファイルを出力する機能もあります。

ファイル読み込み・字句解析・構文解析・型検査・ファイル書き出しとそれらのエラー報告をSATySFi側がやってくれるので、パッケージ作成者はデータをどう変換すればよいかだけを考えれば良いのでとても便利です。


# さっそく使ってみる

さて、グダグダいう前に早速使ってみたいと思いますが、さすがにいくつか事前知識が必要なのでまずはそれについて説明します。

## 起動

```console
$ satysfi --text-mode 〈形式名称〉 〈入力ファイル名〉 -o 〈出力ファイル名〉
```

という形でSATySFiのテキスト出力モードが起動します。

拡張子の自動判定ができないので出力ファイル名は省略できません。

形式名称は変換先のテキストファイルの形式を指定するもので、`"html"`や`"platex,latex"`のようにカンマ区切りで入力します。この時、より限定的な形式を先に書くようにします。


例として、`test.saty`ファイルを処理して`test.txt`というテキストファイルを出力するようにしたいときは

```console
$ 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"`であったときは

1. `fuga.satyh-platex`
1. `fuga.satyh-latex`
1. `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ファイルだって作れそうです。}
>
```


これらができたら

```console
$ 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](https://github.com/gfngfn/SATySFi/blob/master/src/frontend/bytecomp/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のテキスト出力モードに手を出してみましょう。