Typstでリンク先のURLを脚注に出したい、自動で
背景
紙版と電子版の両方のフォーマットで出力することが想定される場合、記事中のリンクのURLを脚注に示したい、ということがあると思います。
Typstではlink
をカスタマイズすることでこれが実現できます。ただ、いくつか難しい点があったので、ここにまとめておきます。試したことを順を追って書いていきますので、結果だけ知りたい方は「まとめ」を見てください。
footnote に URL を出す
まず思いつくのは以下のようなやり方です。
// 大体それっぽく動く
#show link: it => {
it
footnote(it.dest)
}
#link("https://example.com")[Example]
これでほとんど期待通りに動きます。ただ、すぐに気がつく問題があります。脚注にある URL はリンクになっていない、つまりクリックしてもリンク先に遷移できないのです。
footnote の中で link() を使う→失敗
そこで、footnote()
の中でlink()
を使う方法を試してみました。しかし、こうすると無限ループが発生するようで、コンパイルが終わらなくなってしまいます。
// これはコンパイルが終わらない
#show link: it => {
it
footnote(link(it.dest, it.dest))
}
#link("https://example.com")[Example]
実は、この他にも問題があります。
冗長な脚注が出力される問題
記事中に URL が書かれていて自動リンクになると、冗長な footnote が生成されてしまうのです。具体例を見てみましょう。
// 冗長なfootnoteができてしまう例
#show link: it => {
it
footnote(it.dest)
}
https://example.com
こうすると、本文には https://example.com 1
と出力され(1は脚注へのリンク)、脚注には 1 https://example.com
が出力されます。本文中にURLが書かれているので、わざわざ脚注にもURLを出す必要はありません。こういうケースは通常のlink
として振る舞ってほしいわけです。
比較演算でいける?→失敗
Typstが自動でリンクを生成する場合は、リンクテキストとリンク先が同一のリンクが生成されるようです。したがって、比較演算子を使って比較すれば良いだろう...と思うのですが、そこまで簡単ではありません。
// うまくいかない
#show link: it => {
it
if it.dest != it.body { // この条件は常に成立してしまう
footnote(it.dest)
}
}
https://example.com
it.dest
は string
型で、 it.body
は content
型なので、常に it.dest != it.body
が成立してしまい、必ず脚注が出てしまいます。
content
をstring
に変換してから比較する
content
をstring
に変換してから比較したいところですが、これがそれほど簡単ではないようです。検索していると https://github.com/typst/typst/issues/2196#issuecomment-1728135476 にやり方を書いているコメントがありました。content-to-string()
に名前を変えつつ、以下のようにしてみます。
// リンクテキストとリンク先が同一の場合はリンクしない
#let content-to-string(content) = {
if content.has("text") {
content.text
} else if content.has("children") {
content.children.map(content-to-string).join("")
} else if content.has("body") {
to-string(content.body)
} else if content == [ ] {
" "
}
}
#show link: it => {
it
if it.dest != content-to-string(it.body) {
footnote(it.dest)
}
}
#link("https://example.com")[Example]
https://example.com
こうすると、#link
で書いた方だけが脚注になります。
脚注がリンクにならない問題、再訪
実は、冗長な脚注が出る問題が解決できると、脚注のURLがクリッカブルではない問題も解決できるようになります。以下のようにします。
// 脚注もクリッカブルになる
#let content-to-string(content) = {
if content.has("text") {
content.text
} else if content.has("children") {
content.children.map(content-to-string).join("")
} else if content.has("body") {
to-string(content.body)
} else if content == [ ] {
" "
}
}
#show link: it => {
it
if it.dest != content-to-string(it.body) {
footnote(link(it.dest, it.dest))
}
}
#link("https://example.com")[Example]
https://example.com
今度は、再帰的にlink
が呼び出されても、リンクテキストとリンク先が一致するケースになるため、脚注がさらに追加されることはありません。これで完成です。めでたしめでたし。
と思ったのですが、まだ問題がありました。Typstのリンクは外部URLだけでないのです。
内部リンクを壊さないようにする
たとえば以下のような原稿があるとエラーが出てしまいます。
// 内部リンクがあるとエラーがでる
#let content-to-string(content) = {
if content.has("text") {
content.text
} else if content.has("children") {
content.children.map(content-to-string).join("")
} else if content.has("body") {
to-string(content.body)
} else if content == [ ] {
" "
}
}
#show link: it => {
it
if it.dest != content-to-string(it.body) {
footnote(link(it.dest, it.dest))
}
}
= chapter
<chapter>
#link(<chapter>)[Internal link]
この場合、 id.dest
が label
型になっています。 type(it.dest)
が string
であることを確認すればよいでしょう。
まとめ
というわけで、まとめると、以下のようにしてリンクのURLを自動で脚注に出力できるようになります。
// 必要に応じてリンクを脚注に自動で出す
#let content-to-string(content) = {
if content.has("text") {
content.text
} else if content.has("children") {
content.children.map(content-to-string).join("")
} else if content.has("body") {
to-string(content.body)
} else if content == [ ] {
" "
}
}
#show link: it => {
it
if type(it.dest) == "string" and it.dest != content-to-string(it.body) {
footnote(link(it.dest, it.dest))
}
}
= chapter
<chapter>
#link("https://example.com")[Example]
https://example.com
#link(<chapter>)[Internal link]
Discussion