はじめに
LaTeXでは、latexdiffという文書間の差分をとる方法が用意されています。
しかし現状のtypstでは、この機能がありません。このような差分の機能は、レビュアーが修正箇所をすぐに判別するために重要です。typstのissues(#3049)でもこの機能について取り上げられており、将来的には実装されると考えられます。
今回は、現状のtypstで文書間の差分を取る方法を紹介します。
diffコマンドを使用する方法
diffコマンドは、コマンドライン上で用意されている文字列の差分をとるツールです。
たとえば
diff a.txt b.txt
とすれば、2つのテキストファイルの差分を取得できます。これを使えばtypstでも差分ファイルが作れます。以下のように書いたdiff.shを作成して実行すれば、typst差分ファイルができます。
#!/bin/bash
A="$1"
B="$2"
OUT="diff.typ"
git diff -U9999 -u --no-index --word-diff=plain "$A" "$B" \
| grep -vE '^(diff --git|index|---|\+\+\+|@@)' \
| sed -E \
-e 's/\{\+([^}]*)\+\}/#text(fill: blue)[\1]/g' \
-e 's/\[-([^]]*)-\]/#text(fill: red)[\1]/g' \
> "$OUT"
例えば、以下の二つの文書
#set text(font: "Harano Aji Mincho")
吾輩は猫である。
名前はまだ無い。
どこで生れたかとんと見当がつかぬ。
何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。
しかもあとで聞くとそれは書生という人間中で一番獰悪な種族であったそうだ。
#set text(font: "Harano Aji Mincho")
吾輩は猫である。
どこで生れたかとんと見当がつかぬ。
何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。
吾輩はここで始めて人間というものを見た。
しかもあとで聞くとそれは書生という人間中で一番獰悪な種族であったそうだ。
を先のスクリプトで差分ファイルを作ると、以下のようになります。
しかし、diffコマンドではtypstの構造を認識できないため、少しでも複雑な文書になると適切な差分ファイルを生成できません。
Pandocを使った差分ツール
Pandocを使うと、比較的簡単に差分typstファイルが作れるようです。これらのパッケージはすでに公開されており、Typstdiffなどで取得できます。先の例に対して、
typstdiff a.typ b.typ diff.typ -if red -df blue -ih gray -dh yellow
のようにして実行すれば、以下のようになります。
しかしPandocで解析できるTypstの文法は限られており、普通書くような複雑な構造の文書ではエラーで差分が取れなくなってしまいます。
AST構造を解析する差分ツール
typst-diff-toolではPandocに頼らず、typstのAST構造に基づいて差分を解析します。これは非常に良くできていて、例えば以下のような少し複雑な二つの文書
#import "@preview/js:0.1.3": *
#show: js
#maketitle(
title: "タイトル",
authors: "名前",
abstract: [
差分を表示するサンプルAです。
]
)
= サンプル文章
吾輩は猫である。
名前はまだ無い。
どこで生れたかとんと見当がつかぬ。
何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。
しかもあとで聞くとそれは書生という人間中で一番獰悪な種族であったそうだ。
$
integral_(-oo)^(oo) e^(-x^2) "d" x = sqrt(pi)
$
#figure(
rect(width: 8cm, height: 5cm, fill: gray),
caption: [画像のサンプル。画像の色は灰色です。]
)
#import "@preview/js:0.1.3": *
#show: js
#maketitle(
title: "タイトル",
authors: "名前",
abstract: [
差分を表示するサンプルBです。
]
)
吾輩は猫である。
どこで生れたかとんと見当がつかぬ。
何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。
吾輩はここで始めて人間というものを見た。
しかもあとで聞くとそれは書生という人間中で一番獰悪な種族であったそうだ。
$
integral_(0)^(oo) e^(-x^2) "d" x = sqrt(pi)/2
$
#figure(
rect(width: 8cm, height: 5cm, fill: luma(230)),
caption: [画像のサンプル。画像の色は明るい灰色です。]
)
についても、以下のように差分が取れます。
おおよそうまく差分が取れているようですが、図が1枚のはずなのに2枚出力されており、図番号が「2」まで出てきています。
また、.typ内でincludeされている.typファイルに対して差分は取れないようです。
Typstで差分をとる自作パッケージ
上記の問題を含む複雑な文書においても差分をとるため、新たに自作パッケージを開発しました。このパッケージでは、複雑な文法構造(パッケージやinclude文を含む)であっても、文章間の差分を取ることができます。Typst Universeに登録したので、以下のように差分が取れます。
#import "@preview/palimset:0.1.0": *
#diff-content(
include "diff-a.typ",
include "diff-b.typ"
)
差分の結果は以下のように表示されます。
このパッケージでは、文書中の地の文だけを抜き出して(他の要素を無視して)差分を取ります。箇条書きや数式等は差分の対象外となっています。これによって、画像や数式が2個出てきて番号付けがズレる、というような問題を回避しています。
パッケージの仕組み
ここからは自作パッケージの仕組みを説明します。
差分アルゴリズムの実装
typstにはそもそも文字列間の差分を取る方法が用意されていないので、ここから実装する必要があります。差分アルゴリズムは様々提案されていますが、比較的スマートでメモリ効率も悪くないMyers (1986)のO(ND)アルゴリズムを選択しました。
実装の際にはレコメンデーションとエディットグラフの記事を参考にしてtypstで実装しました。
日本語の差分は文字単位で、英語の差分は単語単位で取りたいので、正規表現
regex("[^A-Za-z0-9]")
を使います。これに当てはまる文字で区切ることで、文字列の正しい差分を取ることができます。文字列の差分はdiff-string関数で実装したので、例えば
#import "@preview/palimset:0.1.0": *
#let a = "hello, workd. こんばんは"
#let b = "hello, world. こんにちは"
#diff-string(a, b)
と書けば以下のように出力できます。
複雑な文書の差分
diff-string関数では単純な文字列の差分を取ることはできますが、章や図・数式といった文章以外の要素が出てきた場合に差分を取ることができません。これらを含む差分を取るときはtypstの文法自体を解析する必要があります。このあたりで悩んでいたところ、cjk-unbreakというパッケージを見つけました。
このパッケージではtypstの構造を解析して、改行間の空き問題を解消しています。パッケージ内にはtransform-childsという構造解析用の関数があり、これを真似すれば差分が取れるのでは?と考えました。
そこで、以下の方針で差分を取ります。
- (更新前の)文書Aと(更新後の)文書Bの中からメインの文章A・Bをそれぞれ抜き出す
- 文章AとBの差分をとる
- 文書Bを出力しつつ、差分要素を装飾
1.ではtransform-childsを参考にして構造解析してtextを抜き出します。この過程で既にテキストだけの文書になるので、2.では先ほどのdiff-stringと同じ差分関数が使えます。
3.ではまたtransform-childsを参考にしながら1.の文章を抜き出した要領で差分要素を装飾していきます。出力する順番は「削除した要素→追加した要素」の順番にしています。
最後に
今回はlatexdiffの機能をtypstで実現するための手段を紹介しました。自作パッケージはまだメインの文章しか差分できず、箇条書き等の要素を無視します。このあたりについて知識や意見があれば、Githubにissueを立てていただけると嬉しいです。




