巷ではよくTypstは「バックスラッシュをあまり書かない組版ソフトウェア」と認識されているようです。しかしもちろん、Typstでも(特に数式モードで)バックスラッシュ(文字「\」=U+005C)が必要になる場合が結構あります。本記事ではTypstの文書ソースにおけるバックスラッシュ文字の用法について解説します。
本記事の例示文書では本文フォントとして「原ノ味明朝」を指定しています。
前提知識
- Typstの3つのモード(マークアップ・数式・コード)についての理解。
マークアップモード/数式モードの場合
マークアップモードおよび数式モードにおいては、バックスラッシュ文字は以下の3つの用法をもちます。
-
\‹空白文字›1は改行(linebreakエレメント)を表します。 -
\u{はUnicodeエスケープを開始します。 - 上記以外の
\‹文字›は特殊文字でない通常の‹文字›自体を表します(リテラル化)。例えば\@は「refの短縮表記」ではない@そのものを表します。
以下でそれぞれの用法について説明します。
改行の短縮表記
\の直後の文字がUnicode空白文字(典型的にはASCII空白(U+0020)や改行文字(U+000A))である場合は、その\は改行の短縮表記と見なされてlinebreakエレメントに置き換えられます。
Typst is great.\ Sushi is delicious.
$ // 数式モードでの用法
x + y &= 96 \
x - y &= 12
$
なお細かい話になりますが、\の後にある空白文字自体はそのまま残ると考えられます。
#repr[A\ B]
しかしTypstの組版規則として改行の直後にあるASCII空白は抑止されます。またタブ(U+0009)や改行は入力規則上でASCII空白に正規化されるため、「\の後がASCII空白・タブ・改行」という典型的なケースでは空白は結局出力されません。これに対して\の後がその他のUnicode空白文字の場合は空白が実際に出力されます。
// "\"の直後に全角空白(U+3000)がある
Typstは\ アレ。
また本当に細かい話ですが、マークアップコードの末尾に\がある場合もそれは改行と見なされます。次のような(やや技巧的な)コードで確かめられます2。
// linbreakで改行せずに代わりに"BREAK"と書く
#show linebreak: "BREAK"
// "B\"でファイルが終了している(改行無し)
A\ B\
Unicodeエスケープ
\u{‹符号値16進›}はその符号値をもつUnicode文字そのもの(リテラル)を表します(Unicodeエスケープ)。
// "\u{2603}"はU+2603の文字"☃"を表す
Here is a \u{2603}.
If $s in { \u{2603} }$, then $s$ is nice.
Unicodeエスケープの開始は\u{で判断されます。直後が{でない\uはUnicodeエスケープにならず、単にuのリテラル化と見なされます。
// これはUnicodeエスケープにならない
Here is a \u {2603}.
\u{でUnicodeエスケープを開始したのに正しい書式で終結していない場合(例えば\u{}や\u{42X}や\u{42等)はエラーになります。
// ざんねん🙃
Here is a \u{2603ZR}.
//==> ❌エラー: invalid Unicode codepoint: 2603ZR
リテラル化
先述の2つの場合以外の\u‹文字›は単に‹文字›そのものを表します。ここで‹文字›が当該のモードにおいて特殊な意味を持つ文字であったとしても\‹文字›は常に特殊でない通常の(リテラル化された)文字を表します。元々特殊でない文字に\を付けても何も意味はないので、普通は特殊文字に\が付けられます。
マークアップモードでのリテラル化の例です。
// これは参照(ref)ではない
Posted from \@zr_tex8r.
// これはコードモード呼出ではない
Post with hashtag \#Typst.
// これは節見出し(heading)ではない
\= ←equal sign
数式モードでのリテラル化の例です。
// "epsilon\/2"はfracの短縮表記ではない
Then we have $0 <= delta <= epsilon\/2$.
$ // この括弧はlrの短縮表記ではない
x^3 + y^3 &= \(x + y\)\(x^2 - x y + y^2\) \
// 比較用: 単純に括弧を書くとlrの短縮表記となり括弧が
// 自動伸長してしまい, この場合はあまり好ましくない😐
x^3 - y^3 &= (x - y)(x^2 + x y + y^2)
$
// 前のコンマは引数区切りではない
$ frac(A\, B, C) $
余談: なぜ自動伸長しない括弧が\(~\)なのか
前節の数式モードでの例で「/と\/」および「(~)と\(~\)」について動作の違いを示しました。
-
/は分子と分母を縦に並べた分数(fracエレメントの短縮記法)。 -
\/は「/」の文字自体。分数を「a/b」のように書くときに使う。 -
(~)は自動伸長する丸括弧(lrエレメントの短縮記法)。 -
\(~\)は自動伸長しない丸括弧。
これらの組については、文書作成者が自分で判断して適切に使い分ける必要があります。(例で示したように、むやみに(~)を使うと不必要に括弧が伸長する3場合がかなり多いのです。)
これを見ると、単純な「/」「(」「)」を表すのが/ ( )になっていないわけで、これは不自然だと思った人もいるでしょう。特に括弧については(~)よりむしろ\(~\)の方が使用頻度は高い4はずなので、単純な方に\を付けているのは不合理にも思えます。
しかし、前節に述べた「Typstでのバックスラッシュの使い方」の原則を理解すれば、この規則になっている理由が解るはずです。Typstでは一部のASCII記号に特別な機能を割り当てた上で、その記号そのものを表すために「リテラル化」のバックスラッシュを使うという原則になっています。この原則に従う限り、「特殊な/と単純な/」があった場合には単純な方に\を付けて\/とする必要があるわけです。
コードモードの場合
コードモードではバックスラッシュは有効な文字に含まれません。つまり、一般的にはバックスラッシュが含まれているとエラーが発生します。
grid("A"\, "B", "C")
//==> ❌エラー: the character `\` is not valid in code
しかし、コードモードの中でも文字列リテラル("Hello\n"のようなstr値の即値の表記)については\を含めることができます。そこで以降では文字列リテラルでの\の用法を説明します。
文字列リテラルでの用法
-
以下の組み合わせ(エスケープシーケンス)は特定の(直接入力が困難な)文字を表します。
組み合わせ 符号位置 説明 \\U+005C バックスラッシュ「\」 \"U+0022 ダブルクオート「"」 \nU+000A 改行文字(LF) \rU+000D 復帰文字(CR) \tU+0009 タブ文字(HT) -
\u{はUnicodeエスケープを開始します。Unicodeエスケープの書式についてはマークアップモードと同じ規定と思っていいでしょう。文字列中のUnicodeエスケープはマークアップモードのものと比べて複雑な挙動を示します。マークアップモードではエラーになる不完全なUnicodeエスケープの入力に対してもエラーにならずに以下のように動作します。
- 直後に16進数字が続かない
\u{はUnicodeエスケープの開始と見なされません。つまり\は特殊用法に該当しないのでそのまま\を表します。 - 閉じる
}が欠落している場合(つまり\u{‹符号値16進数字列›の後に}がないパターン)は}がある場合と同様に扱われ、その符号値をもつUnicode文字を表します。
1はともかく2はかなり不自然であり将来“修正”が入るかもしれません。現状の挙動には依拠せずに「マークアップモードでエラーになる書き方」は文字列リテラルでも使わないのが無難でしょう。
- 直後に16進数字が続かない
-
上述の場合以外では、
\はそのまま\の文字を表します。例えば"\#"は文字通りで「\#」という文字列を表します。マークアップモードの\とは仕様が異なる5ので注意が必要です。
// 文字列を符号値の列に変換する関数
let dump(s) = s.codepoints().map(str.to-unicode)
// エスケープシーケンスの例
dump("A\\\"\n\r\tB")
//==> (65, 92, 34, 10, 13, 9, 66)
// Unicodeエスケープの例
#dump("A\u{20000}B") \
//==> (65, 131072, 66)
// これはそのまま「A\*\a\xB」という文字列
#dump("A\*\a\xB") \
//==> (65, 92, 42, 92, 97, 92, 120, 66)
// これはそのまま「A\u42B」という文字列
#dump("A\u42B")
//==> (65, 92, 117, 52, 50, 66)
余談: バックスラッシュだらけの文字列を簡単に扱う術
Typstのスクリプトにおいて「バックスラッシュを大量に含む文字列」を扱いたい場合があります。例えば正規表現パターンやWindowsのパス名を扱う場合6が該当します。
例えば「\[(\w+)\]:\s*(\d+)\n」という正規表現(regex値)を使いたいとします。するとこのパターンを文字列リテラルで書くことになります7が、\を大量に書く必要があり、これはいかにも面倒です。
// これは面倒🤔
let rx = regex("\\[(\\w+)\\]:\\s*(\\d+)\\n")
この問題の解決策として、生テキスト(`~`記法で表される文書要素)を利用する方法があります。ご存じの通り、`~`記法ならどんな文字でもそのまま書けます。生テキスト(rawエレメント)自体は文書要素(content値)であり文字列(str値)ではありませんが、「中身の文字列」を表すtextフィールドがあります。これを利用して、 `~`.textとすれば~の部分の文字列を取り出せます。
// `~`.text は"~"に書いた文字列
let rx = regex(`\[(\w+)\]:\s*(\d+)\n`.text)
// 想定通りに動作する
"[answer]: 42\n".match(rx).captures
//==> ("answer", "42")
長い形式(```~```)の生テキストも利用できます。こちらの場合は開始の```の直後の改行が無視される8ので複数行にわたる文字列の記述に便利でしょう。
// 複数行にわたる文字列データ
let data = csv(bytes(```
foo,42
bar,54
```.text))
// 想定通りに動作する
data.at(0)
//==> ("foo", "42")
実は、正規表現のパターンの場合は「余分の\を追加せずにそのまま"~"で囲って文字列リテラルにする」という方法でも問題なく動作します。
// "~"の中にそのまま書く😐
let rx = regex("\[(\w+)\]:\s*(\d+)\n")
// それでも想定通りに動作する😲
"[answer]: 42\n".match(rx).captures
//==> ("answer", "42")
前節で述べた通り、Typstの文字列リテラルでは特殊な意味を持たない\はそのまま維持されるので、\[や\wの部分はそのまま解釈されます。\nの部分は文字列のエスケープシーケンスなのでU+000Aの文字に置き換わりますが、正規表現で\nが表しているのもU+000Aの文字なのでパターンの意味に影響はありません。Typstの文字列リテラルで特殊な意味を持つ組み合わせ(\\、\"、\n、\r、\t、\u{‹符号値›})は全てTypstの正規表現でも同じ意味をもつため、文字列リテラルの\の解釈で意味が変わることは決してないわけです。
まとめ
「なんか、余談の方が分量が多くなっているぞ」(ざんねん
)
-
マークアップテキストの末尾の
\もこの項に含まれます。 ↩ -
あるいは
evalを使ってrepr(eval(mode:"markup", "A\\"))の結果を調べるという手もあります。ここでrepr[A\]では\が]に付いていると見なされて失敗します。なお、例示コードでは最初の“BREAK”と“B”の間に空きが入っていて、これからも空白文字がそのまま残ることが確かめられます。 ↩ -
例では
(x^2 - x y + y^2)のように添字付き文字に(~)を適用すると括弧が不必要に伸長することを示しました。これに対して、「それは単に\left(~\right)の記述を面倒がったLaTeX者が伸長させないだけで、本当は伸長する方が組版的に適切ではないか」と思った読者がいるかもしれません。そういう人は高校数学の教科書や参考書(LaTeX・Typstの組版でないもの)を見てみましょう。添字付き文字のケースでは括弧は伸長していないはずです。 ↩ -
これは「伸長しない」と判断した括弧を全て
\(~\)で書いた場合の話です。「そもそも伸長が起こらない場合は(~)で書く」という方針も可能ですが、これだと「伸長が起こるかどうか」もユーザが判断する必要が生じて面倒です。 ↩ -
マークアップモードではほぼ全ての文字に対して
\がリテラル化の機能を果たすのに対して、文字列リテラルでは「実際に文字列リテラル中で特殊な意味を持つ文字」である\と"に対してのみ\がリテラル化の機能を果たす、といえます。 ↩ -
あるいは「バックスラッシュを大量に書く組版ソフトウェア🍣」のコードをTypstで扱う場面があるかもしれません。 ↩
-
Typstでは正規表現の値(regex値)は
regex(‹パターン文字列›)のように文字列値(str値)から生成するため、一旦パターンを文字列値として表す必要があります。 ↩ -
長い形式の生テキストでは言語タグ(開始の
```の直後に書く)が指定できます。文字列リテラル代用の用法では言語タグは意味を持ちませんが、```jsonのように指定しておくと一部の開発環境(Timymist等)では文字列の部分にハイライトが効きます。 ↩







