SATySFiでシンタックスハイライトが効くコード貼り付けパッケージ(code-printer)の紹介

はじめに

今年も始まりましたSATySFi Advent Calendar 2021

今年もSATySFi関係の記事を充実させていきましょう。

そろそろSATySFi v0.1.0のリリースがあるということで、これからも楽しみですね。

さて、今回はSATySFiでソースコードを表示するためのcode-printerというパッケージを作成したので、これについて記事を書いていきたいと思います。

パッケージのリポジトリは"puripuri2100/satysfi-code-printer"にあり、satyrographos-package-indexでのページは"packages/code-printer"にあります。

最新版のマニュアルはpuripuri2100.github.io/satysfi-code-printer/code-printer-ja.pdfにあります。

記事執筆時点でv1.0.0です。 v2.0.0以降のバージョンはおそらく挙動が変わっているでしょうから、この記事はあまり役に立たないでしょう。

このパッケージの強み

このパッケージはLaTeXでいうところのlistingsパッケージと似たような機能を持ち、さらにSATySFiならではの独自の強みも持っています。すなわち、

  • 言語によるシンタックスハイライトを提供する
  • フォント・フォントカラー・背景色などのテーマを変更できる
  • コード枠や行番号などの装飾を変更することができる
  • 高速に動作する
  • マルチバイト文字列もある程度対応している(結合する文字への対応は不可能)
  • 40程度のシンタックスハイライトと20強のテーマをデフォルトで提供している
  • ユーザーが独自にシンタックスハイライトやテーマを作成することが可能

という特徴があります。

インストール

SATySFiのパケージマネージャであるSatyrographosを用いてインストールすることを想定しています。

v0.0.6リリース後に追加されたread-fileという外部ファイル読み込みプリミティブを使用しているため、v0.0.6-53-g2867e4d9以降のSATySFiでなければなりません。

opam update

opam install satysfi-code-printer

satyrographos install

と、それぞれ実行することでインストールされるはずです。

もしSATySFiをリポジトリから直接ビルドして使っていてタグがv0.0.6のままの人は、

opam pin add "git+https://github.com/puripuri2100/satysfi-code-printer.git"

opam install satysfi-code-printer

satyrographos install

をしてcode-printerのリポジトリから直接インストールしてください(リポジトリのHEADではv0.0.6のままでインストールができるようなバージョン制約がopamファイルに書いてあるからです。)

もしSATySFi v0.0.7がリリースされ、それを使うことになった場合は

opam pin remove satysfi-code-printer

opam update

opam install satysfi-code-printer

として、satyrographos-repoの制約のほうを使うことを推奨します。

超簡単な使い方

プリアンブルに

@require: code-printer/code-printer

と書くことで提供されるコマンドを使用することができます。 モジュール名はCodePrinterです。

提供するコマンドは

  • \inline-code
  • +code-printer
  • \code-printer
  • +file-printer
  • \file-printer

の5つです。

これらの5つのコマンドの型は全て

[code-printer-config?; string]

となっています。

オプション引数で設定を渡し(渡さないこともでき)、コードを与えることで組版されます。+file-printer\file-printerについては、コードではなく貼り付けたいファイルのパスを与えます。

シンタックスハイライトをつけたり、カラーテーマを変更したり、デザインを変更したりすには、オプション引数で設定を与える必要があります。

言語・テーマの設定の仕方

+code-printer ?:(
  CodePrinter.make-config syntax theme
) (`code`);

のようにして言語とカラーテーマを選択することができます。

言語はデフォルトでいくつか用意されており、

@require: code-printer/code-syntax

で読み込んだパッケージ(提供モジュール名はCodeSyntax)で40個弱提供されています。詳細はドキュメントやREADMEを見ていただくとして、一例を挙げるとこのようになっています。

f:id:puripuri2100:20211201200945p:plain
プログラミング言語のsyntaxの例

カラーテーマはデフォルトでいくつか用意されており、

@require: code-printer/code-theme

で読み込んだパッケージ(提供モジュール名はCodeTheme)で20個強提供されています。詳細はドキュメントを見ていただくとして、一例を挙げるとこのようになっています。文字色やフォント、背景色を変更することができます。

f:id:puripuri2100:20211201201023p:plain
カラーテーマの例

例えば、SATySFiのコードをダークテーマで組みたいと思ったら

+code-printer ?:(
  CodePrinter.make-config CodeSyntax.satysfi CodeTheme.basic-dark
) (`let-mutable v-ref <- 0
let f x = x + 1
let g =
  let () = v-ref <- (f 1) in
  f !v-ref`);

みたいにすると

f:id:puripuri2100:20211201201054p:plain
貼り付けられたコードの例

のようになります。

全てのシンタックスとカラーテーマをみたい場合には“ドキュメントPDF”をお読みください。

その他のデザインの設定の仕方

設定用関数で継ぎ足すようにして設定を行っています。

例えば、行番号の表示を変更するには

+code-printer ?:(
  CodePrinter.make-config
  CodeSyntax.satysfi
  CodeTheme.basic-light
  |> CodePrinter.set-number-fun (fun ctx i -> (
    let ctx = set-text-color Color.black ctx in
    let number-it = i |> arabic |> embed-string in
    read-inline ctx {#number-it; 行目 }
  ))
) (`let x = 1 in
x * 2`);

のようにすると

f:id:puripuri2100:20211201201119p:plain
行番号変更のデモ

のように変更できます。

他にも

  • フォントサイズを編子する鵜機能
  • 行ごとに背景色を変更する機能
  • 改行マークの変更
  • タブ文字のサイズの変更
  • 枠などの装飾や周囲の余白の設定

ができるようになっています。

詳しくはドキュメントを読んでください。

オンライン上で読むには“ドキュメントPDF”をお読みください。

ローカルで見るには、

opam install satysfi-code-printer-doc

satyrographos install

を実行することでドキュメントファイルがビルドされて配置されます。

使用した技術・テクニックなど

設定の方法

デザインの設定には、今年に行ったSATySFi Conf 2021で紹介された「パイプライン演算子と関数を用いたDSL」を利用しました。

default ()
|> f1
|> f2
|> f3
...

という風にして設定を複雑化させていく手法です。Rustのライブラリでも採用されている手法です(clapなど)。

この手法では

  • 設定の順序を気にしなくて良い
  • 設定項目の増減を破壊的変更無しに実現できる
  • 見た目でわかりやすい

といった利点があります。

シンタックスハイライトを実現するために

正確なシンタックスハイライトを実現するには一からlexerを言語ごとに書いていく必要があります。しかし、そんなことをすると対応言語を増やすコストが増えてしんどくなります。

そこで、ある程度の正確性を犠牲にして簡単に設定できるようにしました。

  • 行コメント
  • ブロックコメント
  • 文字列
  • キーワード
  • 識別子

の5つそれぞれに対応するルールを書くだけで設定が終了するようになっています。ルールは主に正規表現で行うことになっています。

正規表現エンジンを自前で実装すると遅くなるので、SATySFi組み込みの正規表現エンジンを利用しています。これにより、他のシンタックスハイライトを実現するソースコード貼り付けパッケージに比べて圧倒的に早く処理が終わるようになっています。

string-scan : regexp -> string -> (string * string) optionというプリミティブを用いて解析を進めていきます。このプリミティブは

  • 与えられたregexpが先頭から続く文字列にマッチする場合はその文字列と残りの文字列を返す
  • マッチしなかった場合はNoneを返す

という役割を果たします。

たとえば、

let r = regexp-of-string `[0-9]+`

let s1 = `123abc456`
let v1 = string-scan r s1

let s2 = `abc456`
let v2 = string-scan r s2

としたとき、v1Some((`123`, abc456))になり、v2Noneになります。先頭から読んでいく解析にぴったりです。

これをルールを順番に適用していくことで、文字列がどの5つのルールに当てはまるのかを確認していき、トークンに変換します。また、そのとき同時にタブ文字・改行文字・スペースの解析もし、この2種類を専用のトークンに置き換えていきます。

この解析でコードがトークン列になるので、あとはカラーテーマの設定に従って組んでいくだけです。

一行が長いコードのための工夫

たまに一行が長すぎてはみ出てしまうコードが存在します。

これに対応するために、

  • ハイフネーションを完全に無効化
  • 適切な位置で問答無用で改行されてほしい
  • 改行箇所に改行マークなどを入れられるようにしたい

という3つの要望を満たしつつ、改行ができるように実装しました。

SATySFiにはdiscretionaryという「行分割の候補位置を作成する」プリミティブが存在します。このプリミティブには

  • 行分割されなかったときの挙動
  • 行分割されたときの、分割位置の直前に入れるブロック
  • 行分割されたときの、分割位置の直後に入れるブロック

の3つを指定することができます。これを用いることで上記の3つの要望を全て満たすことができます。

手法としては「コードを一文字ずつ分割してブロックし、discretionaryによって生成された分割候補位置を間に挟み込んでいく」というものです。

これによって、ハイフネーションを無くし、適切な位置で改行させることができます。また、分割位置の前後にブロックを入れられるため、改行マークなどの描画もできるようになります。

おわりに

コードを綺麗に貼り付けるためのSATySFi用のパッケージを作成しました。

かなり高機能なので使っていただけると嬉しいです。

パッケージ同士の機能ごとの切り分けや依存関係の調節など、大変なこともありました。

しかし、結果としてかなり満足な機能に仕上がり、とても嬉しいです。

大学への出願後の解放感で作成したパッケージなので、満足な仕上がりで良い題材になるのにも関わらず、自己推薦書に書けなかったという後悔があります(まぁ良いのですが)。

各言語用のシンタックスを登録するためにかなり多くの言語の公式ドキュメントを読み漁りました。言語によって行きつきやすさがかなり違いました。ここら辺は学ぶところがありますね。もし、言語のシンタックスルールが間違っている箇所がありましたら、プルリクエストを送っていただけると嬉しいです。

色覚異常があっても見やすいカラーテーマを考えるのも大変でした。結局色だけではなくフォントも変更することで判別しやすくしました。ここら辺はもうちょっと考えていきたいところですので、ご意見お待ちしております。

以上、シンタックスハイライトが効くコード貼り付けパッケージの紹介でした!