サイトの 外部リンクを 別窓に する for Sphinx
ちょっとSphinx用の拡張コードを書いて、 サイト内にある外部サイト [1] へのリンクを別タブ(別窓)で開くようにしました。 今のところは、ライブラリとしての公開は一拍置く予定で、実装の考え方をメモがてら書き置きします。
Sphinx(rST)に おける リンクの 考え方
Sphinxでのリンクの書き方はいくつかありますが、 このあたりの内容自体は Sphinx日本ユーザー会のサイト を参照してもらったほうが早いので割愛します。
それぞれの書き方をすると、リンク先・リンク内テキスト・その他要素などのパースを行い、
reference
というノードに変換します。
この参照ノードを元に、後述ビルダー達は各種出力を行います。
Sphinxの HTMLビルドに おいて、 リンク=a要素は どのように 生成されるか
Sphinxでアウトプットをどのようにするかは、主にビルダーの担当となっています。 雑に書くと、HTMLを生成するためには、このような流れになっています。
html
ビルドを用いてStandaloneHTMLBuilder
によるビルドを実行ビルダーは出力のための各種準備を行い、
HTML5Translator
というライターに出力を委譲
HTML5Translator
には、ノードごとに
visit_xxxx
, depart_xxxx
といったメソッドが定義されており、
文字通り、ノードの開始/終了時に随時呼び出される形式になっています。
今回のリンクなどは前述の通り reference
ノードです。
HTML5Translatorのコード
には visit_reference
というメソッドが確認でき、
ここで a
要素を生成していることが分かります。
処理を 継ぎ足して、 無理矢理外部リンク化する
現在では、外部リンクを別窓などで開くための a
要素には、
target="_blank"
だけでなく rel="noreferer"
を指定することが多いです。
しかし、 HTML5Translator.visit_reference
の実装には、 rel
属性の入り込む余地がありません。 [2]
そのため、「まるごと実装を差し替える」「一度出力して置換する」などの強硬策で別窓化する必要があります。 今回は「まるごと実装を差し替える」形式で実現しています。
def visit_reference(self, node):
atts = {"class": "reference"}
if node.get('internal') or 'refuri' not in node:
atts['class'] += ' internal'
else:
atts['class'] += ' external'
# overwritten
atts['target'] = '_blank'
atts["rel"] = "noreferrer"
もともとの HTML5Translator.visit_reference
が定義している実装をまるまるコピペしています。
その上で、内部リンクor外部リンクの判定をしているブロックを見つけ、
外部リンクだった際に属性2個を追加しています。
from sphinx.writers.html5 import HTML5Translator
# 中略
def setup(app):
app.add_node(
reference, True, html=(visit_reference, HTML5Translator.depart_reference)
)
reference
ノードに対する開始/終了の処理ペアを、
開始のみ実装したものに上書きします。
これで、外部リンク判定のものに対してはHTML生成時に target="_blank"
等が付与されるようになります。
ライブラリ化を 躊躇している 理由
それなりに便利な実装なのですが、現状だとプライベートPyPI含めてパッケージ化していません。
このあたりの理由は大きくは3つあります。
テスト・構造を一切度外視して結果のみを優先したので、想定外のケースへの対処を何もしていない
もうちょっとマシな実装がありそうな気がする(コードのコピペ実装すぎるので、堅牢性がなさすぎる)
優先順位が低い
「マシな実装」が思い浮かびそうなら、冬以降に考えてみようと思います。
※脚注