📝

Typst で JSON をシンタクスハイライト

2023/04/17に公開

組版ソフト Typst には Raw text というものがありこれは Markdown で言うところのコードブロックである。

表示の仕方をカスタマイズするには #show を使ってマッチしたものに対する処理を書けば良く、たとえば単に正規表現にマッチするキーワードを強調するだけなら以下のように書ける。

#show raw.where(lang: "foo_lang"): it => [
    #show regex("\b(for|to|begin|end)\b") : keyword => text(weight:"bold", keyword)
    #it
]

```foo_lang
for i=1 to 10
begin
    print(i)
end
```

しかしもう少し複雑な言語になると色々と難しい。 Typst のスクリプトで複雑な構文解析を書くのはさすがにしんどいし、構文解析は厳密であれば良いというものでもない。 ドキュメント中に出てくるプログラムは大抵の場合にプログラムの一部であるし、ときには間違いの例を書くこともあるのでそういったものもほどほどには処理できる必要がある。 いっそ手作業でマークアップしたほうが楽ということもあるだろう。

カスタムシンタクスハイライトを試してみるにあたって文法が簡単なものの例として JSON に対して試みてみることにした。 大まかな方針としては

  • トークナイズは仕様に厳密にする (失敗したらエラーにする)
  • 構造に対する判断はしない

ということにした。 JSON ではトークンの種類がわかればハイライトには十分であるので。

#show raw.where(lang: "json"): it => {
    let result = ()
    let next = it.text

    while(next != "") {
        // dictionary key string
        let m = next.match(regex("^([\r\n\t ]*?\"(?:\\\\[bfnrt\"]|\\\\u[0-9a-fA-F]{4}|[^\\\\\"])*?\")([\r\n\t ]*?:)"))
        if(m != none) {
            result.push(text(gray)[#m.captures.at(0)])
            result.push(text(black)[#m.captures.at(1)])
            next = next.slice(m.end, next.len())
            continue
        }
        // string other than key string
        let m = next.match(regex("^[\r\n\t ]*?\"(?:\\\\[bfnrt\"]|\\\\u[0-9a-fA-F]{4}|[^\\\\\"])*?\""))
        if(m != none) {
            result.push(text(blue)[#m.text])
            next = next.slice(m.end, next.len())
            continue
        }
        // number
        let m = next.match(regex("^[\r\n\t ]*?-?(?:0|[1-9][0-9]*(\\.[0-9]+)?([eE][-+]?[0-9]+)?)"))
        if(m != none) {
            result.push(text(red)[#m.text])
            next = next.slice(m.end, next.len())
            continue
        }
        // identifier
        let m = next.match(regex("^[\r\n\t ]*?(?:true|false|null)\\b"))
        if(m != none) {
            result.push(text(green)[#m.text])
            next = next.slice(m.end, next.len())
            continue
        }
        // some punctuation
        let m = next.match(regex("^[\r\n\t ]*?[\\[\\]{}:,]"))
        if(m != none) {
            result.push(text(black)[#m.text])
            next = next.slice(m.end, next.len())
            continue
        }
        let m = next.match(regex("^[\r\n\t ]*?$"))
        if(m != none) {
            next = next.slice(m.end, next.len())
            continue
        }
        assert(false, "Unknown token as json.")
    }

    block[#result.join()]
}

= Example of JSON hilighting

```json
{"persons" :
 [{ "name": "tanaka",
    "kanji": "\u7530\u4E2D",
    "age": 24,
    "married": true},
  { "name": "suzuki",
    "kanji": "\u9234\u6728"
    "age": 16,
    "married": false}]}
```

= Example of escape

```json
["\"double quote",
 "and another \n\t\b"]

```

= Example of Number

```json
[123e4, -42.195e+10, 123e-45]
```

= Example of invalid JSON -- Lacked separator

```json
{"foo" "bar"}
["foo" "bar"]
```

= Example of Invalid JSON -- Extra close bracket

```json
[42, "foo"]]
```

オブジェクトのキーであるような文字列とそうでない文字列は区別しているが、区別の方法は「コロンが続くかどうか」なので波括弧の中であってもコロンが欠けていれば普通の文字列という扱いになる。 正しい JSON ならばあり得ない状況だが間違った JSON でどういった判断をして欲しいかを自動的に判断することは出来ない。 いい感じに柔軟に判断する上手い方法があったりするだろうか。

Discussion