Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

Tree-sitterでシンタックスハイライトしたコードをHTMLで出力するワンライナー

Tree-sitterというソフトウェアがある。CとRustメインで書かれているパーサジェネレータとそれをとりまくツールである。

tree-sitter.github.io

これを利用して、コードをもとにシンタックスハイライトを効かせたHTMLを生成できたのでその紹介をする。

追記(2023-05-16T10:43+09:00)

  • tree-sitter単体でもHTMLを出力できたのでその旨追記した。

Tree-sitterのアーキテクチャ

Tree-sitterはコアとなるソフトウェアと、各言語ごとの文法ファイル、そしてそれを実行するCLIが別個のモジュールとして提供されており、文法ファイルをビルドするとそれが使えるようになる、というモジュラーな設計になっている。

例えば、Scalaの文法ファイルはtree-sitter/tree-sitter-scalaで提供される。

github.com

そして、Tree-sitter自体はライブラリの形をしているので、各言語バインディングを使ってそれをC API経由で呼び出すという形になっている。言語バインディングは豊富で、Emacsから呼び出せたりPerlから呼び出せたりする。もちろん、Rustからも呼び出せる。

flowchart TB
tsc["Tree-sitter (CLI)"]
ts["Tree-sitter (Library)"]
tss["tree-sitter-scala (文法定義)"]
tssb["Scalaパーサ"]
text["シンタックスハイライトされた文字列"]

tsc -- "call" --> ts
tss -- "provide grammar info" --> ts
ts -- "generate" --> tssb
tssb -- "parse Scala" --> tsc
tsc -- "highlighting" --> text

Rust製CLIツール

そんなTree-sitterの言語バインディングだが、CLIとして動作するように設計されたRust製のバインディングが存在する。

github.com

tree-sitterリポジトリにあることからも分かるように、ほぼ公式の立ち位置にあるソフトウェアだ。

このCLIツールの面白い機能として、単体でシンタックスハイライトを動作させるという機能がある。これを使うと、任意の(Tree-sitterが文法を認識できる)ソースコードを読み取って、これにシンタックスハイライトを行って標準出力に吐き出してくれる。

事前準備(CLIツール)

まずは前述のCLIツールをインストールする。

$ cargo install tree-sitter-cli

といってもcargoを使うだけでよい。自分がインストールしたときはちょっとRustのバージョンが古かったのでrustupを使って最新のRustをインストールした。

次に、CLIツールの設定を初期化する。これは最初に一度やればよい。

$ tree-sitter init-config

すると、Linuxの場合は~/.config/tree-sitter/config.jsonが生成される。

{
  "parser-directories": [
    "/home/windymelt/github",
    "/home/windymelt/src",
    "/home/windymelt/source",
  ],
  "theme": {
    "attribute": {
      "italic": true,
      "color": 124
    },
    "punctuation.bracket": 239,
    "comment": {
      "color": 245,
      "italic": true
    },
    "function": 26,
    "tag": 18,
    "type.builtin": {
      "color": 23,
      "bold": true
    },
    "number": {
      "color": 94,
      "bold": true
    },
    "type": 23,
    "embedded": null,
    "constant.builtin": {
      "bold": true,
      "color": 94
    },
    "constructor": 136,
    "keyword": 56,
    "constant": 94,
    "property": 124,
    "module": 136,
    "operator": {
      "bold": true,
      "color": 239
    },
    "function.builtin": {
      "bold": true,
      "color": 26
    },
    "punctuation.delimiter": 239,
    "string.special": 30,
    "string": 28,
    "variable.builtin": {
      "bold": true
    },
    "variable.parameter": {
      "underline": true
    }
  }
}

このうち、parser-directoriesに注目してほしい。ここで指定したディレクトリに、各言語の文法が定義されたリポジトリが置かれている必要がある。ここでは追加で"/home/windymelt/src/github.com/tree-sitter"を指定した。

また、テーマを指定したい場合はthemeで設定することになるが、ここではいったんデフォルトのままとした。

事前準備(文法)

ここではScalaの文法を使うことにする。したがって、tree-sitter/tree-sitter-scalaをcloneしておく。

$ cd ~/src/github.com/tree-sitter
$ git clone git@github.com:tree-sitter/tree-sitter-scala.git

次に文法が正しく動くか検査する。

$ cd ~/src/github.com/tree-sitter/tree-sitter-scala
$ tree-sitter test

問題なく終わるはず。

シンタックスハイライト

シンタックスハイライトを行う最低限の準備が完了した。この状態でtree-sitter highlight Foo.scalaを実行すると、標準出力にシンタックスハイライトされたScalaのコードが表示される。

好みでテーマを設定すれば、好きな色で表示することもできる。

tree-sitter.github.io

HTMLに出力する(その1)(追記)

tree-sitter highlight --htmlコマンドを使うとHTMLでシンタックスハイライトを実行し、結果を標準出力に流してくれる。

# -Hは--htmlのshorthand
$ tree-sitter -H Foo.scala > foo.html

このスクショはテーマをちょっといじってから撮影したので色が変化しています

<!doctype HTML>
<head>
  <title>Tree-sitter Highlighting</title>
  <style>
    body {
      font-family: monospace
    }
    .line-number {
      user-select: none;
      text-align: right;
      color: rgba(27,31,35,.3);
      padding: 0 10px;
    }
    .line {
      white-space: pre;
    }
  </style>
</head>
<body>

<table>
<tr><td class=line-number>1</td><td class=line><span style='color: #ea6962'>package</span> <span style='color: #d8a657'>com</span><span style='color: #928374'>.</span><span style='color: #d8a657'>github</span><span style='color: #928374'>.</span><span style='color: #d8a657'>windymelt</span><span style='color: #928374'>.</span><span style='color: #d8a657'>zmm</span>
</td></tr>
<tr><td class=line-number>2</td><td class=line>
</td></tr>
<tr><td class=line-number>3</td><td class=line>import cats<span style='color: #928374'>.</span><span style='color: #d8a657'>effect</span><span style='color: #928374'>.</span><span style='color: #d8a657'>ExitCode</span>
</td></tr>
<tr><td class=line-number>4</td><td class=line>import cats<span style='color: #928374'>.</span><span style='color: #d8a657'>effect</span><span style='color: #928374'>.</span><span style='color: #d8a657'>IO</span>
</td></tr>
...

基本的にこれでも十分に便利そうだ。

HTMLに出力する(その2)

さて、行番号なしのコードがHTMLで欲しいときがある。Tree-sitterの機能を使わなければ、theZiz/ahaというツールを使うことで標準出力をそのままHTMLに変換できるため、Tree-sitterで好みのテーマでシンタックスハイライトしてもらい、HTMLとして出力するという組み合わせができる。

github.com

aha自体はディストリビューションのリポジトリに標準で入っていることが多いようで、自分もzypperでインストールできた:

$ sudo zypper install aha

ahaを単体で使うとXHTMLが出力され、html要素やbodyタグもいっしょに付いてくる:

$ tree-sitter highlight Foo.scala | aha > foo.html
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!-- This file was created with the aha Ansi HTML Adapter. https://github.com/theZiz/aha -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="application/xml+xhtml; charset=UTF-8"/>
<title>stdin</title>
</head>
<body>
<pre>
<span style="color:#5f00d7;">package</span> <span style="color:#005f5f;">com</span><span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">github</span><span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">windymelt</span><span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">zmm</span>

import cats<span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">effect</span><span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">ExitCode</span>
import cats<span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">effect</span><span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">IO</span>
import cats<span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">effect</span><span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">IOApp</span>
import com<span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">monovore</span><span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">decline</span><span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">Opts</span>
import com<span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">monovore</span><span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">decline</span><span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">effect</span><span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">CommandIOApp</span>
...

ちゃんとブラウザでも綺麗にレンダリングされている。

これでも十分有用だが、-nオプションを使うとpre要素まで全部剥がしてくれるため、これを使った出力を改めてpre要素に詰め込むという手法を取るとプログラマチックに動かせて便利だ:

$ tree-sitter highlight Foo.scala | aha -n > foo.html
<span style="color:#5f00d7;">package</span> <span style="color:#005f5f;">com</span><span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">github</span><span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">windymelt</span><span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">zmm</span>

import cats<span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">effect</span><span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">ExitCode</span>
import cats<span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">effect</span><span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">IO</span>
import cats<span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">effect</span><span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">IOApp</span>
import com<span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">monovore</span><span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">decline</span><span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">Opts</span>
import com<span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">monovore</span><span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">decline</span><span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">effect</span><span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">CommandIOApp</span>
import org<span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">http4s</span><span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">syntax</span><span style="color:#4a4a4a;">.</span><span style="color:#005f5f;">header</span>
...

例えばテンプレートエンジンから呼び出すと、テンプレートエンジンにシンタックスハイライト機能を搭載できる。

まとめ

  • tree-sitterは、C/Rustで書かれたパーサジェネレータ/パースライブラリである。
  • tree-sitterは実行時はプリビルドされた文法を使ってパースだけ行うため、非常に高速に動作する。
  • tree-sitterに付属する同名のCLIツールによって、シンタックスハイライトを行うことが可能である。
  • tree-sitterは依存性が少ない(パーサ生成のためにnodeが、パーサの実行のためにCコンパイラが必要だが、依存性地獄とはほど遠い印象だ)。
  • tree-sitter(とahaとを組み合わせること)で、任意のソースコードをシンタックスハイライトしたHTMLが得られる。

あわせて読みたい

eed3si9n.com

★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?