📖

atsphinx-linebreakは、改行を改行として扱います

2024/12/07に公開

少し前に軽く作ってみたSphinx拡張の紹介です。(またです)
この記事は、日本語で書いた初回リリースノートという扱いとなります。

何を作ったか

https://pypi.org/project/atsphinx-linebreak/

atsphinx-linebreakというSphinx拡張です。
大雑把に書くと、「reStructuredTextやMarkdownのソース」に含まれている改行を、ビルド後のコンテンツ内でも改行として扱います。

使い方

過去に自分がSphinx拡張の中でも、飛び抜けてシンプルです。
環境にインストールを行い、conf.pyextensionsに追加する。たったこれだけです。

インストール

PyPI上にアップロードされているので、普通の手法でインストールできます。

pip install atsphinx-linebreak

有効化

Sphinxドキュメント上での有効化も、ごくごく標準的な方法で可能です。

conf.py
extensions = [
    ...,  # 他のSphinx拡張
    "atsphinx.linebreak",
]

この記事を書いている時点では、conf.py上での追加設定や、ソースに対する特殊な編集を必要とはしません。
その代わり、全てのソースに対して影響を与えるため、利用には注意が必要です。

動作について

source.rst
このテキストをビルドすると、単一の行で構成された段落として出力されます。
ソース内で改行をされていても、HTML上では改行として扱われません。

上記のコードに対して、有効にしていない場合はこのように出力されます。

output-before.html
<p>
  このテキストをビルドすると、単一の行で構成された段落として出力されます。
  ソース内で改行をされていても、HTML上では改行として扱われません。
</p>

有効にした状態でビルドすると、ソースにあった改行コードが<br>要素として出力されるようになります。
つまり、概要に書いた通り【文中の改行】を【コンテンツの改行】とみなすようになります。

output-after.html
<p>
  このテキストをビルドすると、単一の行で構成された段落として出力されます。
  <br>
  ソース内で改行をされていても、HTML上では改行として扱われません。
</p>

作った背景

極めてシンプルな作りのため、機能面だけだと淋しいので少し背景周りの話をしてみます。

マークアップ言語における【文】と【段落】と【改行】

標準的なreStructuredTextやMarkdownは文書構造として【文】と【段落】を認識できるようになっています。
また、編集者の視点に立っているからなのか、段落内での改行は改行とみなしません。

source.rst
reStructuredTextにおいて、この文と次の文は【段落】とみなされます。
そのため、ソース上は間に改行コードを差し込んでいますが、HTMLビルド時に改行タグは差し込まれません。
source.md
Markdownでも、この文と次の文は【段落】とみなされます。
そのため、ソース上は間に改行コードを差し込んでいますが、
HTMLビルド時に改行タグは差し込まれません。

ここから先は「別の段落」となるため、ビルド時は間に空間が出来ます。
この空間もbrタグというわけではありません。

HTMLビルドの過程では、この【段落】は文字通り<p> ~~ </p>要素として扱われます。
一方でソース内の【改行】は、文字コードとしての【改行】としてしか扱われません。
結果的に、この改行はHTML内では特に何も作用しないことになります。

なお、「出力時に改行対象とする」事自体は可能となっており、それぞれ下記のような記述が可能です。

source.rst
| 先頭に ``|`` + 空白がある行が続く場合、
| ブロック内の改行はビルド時に【改行対象】とみなされます。
source.md
文末に `\` が挿入されていると、 \
改行として扱われることが多いです。

※個人的には、この仕様や前提となる解釈のほうが好きです。

GitHub Flavored Markdown とその眷属

そんな中で、とある存在が登場します。【GFM】ことGitHub Flavored Markdownです。

Markdownの中でもひときわ有名となったこの方言は、仕様書を読む限りでは【文】の概念を持っていません。 [1]
その代わりに、【段落】内の要素は【行】となっています。
そのため、【段落】中に改行が発生すると「新しい【行】の開始」とみなすため、自動的に改行対象となります。

個人的にはこの仕様が好きではないのですが、 厄介なことにGFMベースの方言や派生パターンがとにかく多数存在します。
実際にZenn内のMarkdown記法も、GFMベースではないのですがパーサーであるmarkdown-itの設定として改行を同じように処理するようになっています。

こうなってしまうと、「改行されているなら改行されてほしい」と考えるのも自然ではあります。
一方で、Sphinxの基本的な挙動としては、この動作には準拠していません。
このあたりの事情を吸収するために作ったのがこの拡張です。

この仕様が好きでない理由

個人的な思想としては、ドキュメントのソースとHTMLについて次のように考えているためです。

  • 文書構造の管理に注力するほうが良い。
  • 特に改行のような画面サイズに依存する内容はレンダリング側の責務である。

この改行に関する仕様は、画面がどのような状態であっても「行末での改行」を強制しています。

個人的には、本来「そこで改行するべきか」というのは画面幅によって計算されるべき要素という考えです。
特定の表現を意図しないケースでこれをされると改行タイミングが不安定になり、体感での可読性が逆に低下するとすら考えることもあります。

このあたりは、電子書籍におけるリフロー型にした際にも起きるかと考えます。

内部構造的な話と余談

最後に、ちょっとした実装内部に関する話と、それにまつわる余談を話します。
GitHubリポジトリは下記の場所にあるため、合わせて読むとちょっと理解がしやすいかもしれません。

https://github.com/atsphinx/linebreak

Sphinxにおける処理プロセスとatsphinx-linebreakの立ち位置

Sphinxはドキュメントビルダーとして振る舞う際に、ソースを直接HTML等にするわけではなく、中間形式であるdoctree [^3] に変換します。
Sphinxのプロセス内の各所に用意されている拡張ポイントに割り込むことで、doctreeの操作などを行うことができます。

[^3] Sphinxのコアとなるdocutilsが内部で処理をする時に使われるASTの形式。

このSphinx拡張は、ソースからdoctreeを生成した後に割り込んで、次のようなことをやっています。

  1. 内部のテキスト要素を順に探索テキストして、残留している改行コードを探す。
  2. 改行コードを見つけたら、そこで分割して【テキスト】【改行】【テキスト】という要素に分ける。
  3. HTML出力時に【改行】要素を見つけたら、<br>として出力する。

余談:改行を改行にするもう一つのタイミング(仮)

実は、Markdownの改行コードを【改行】として扱えるようにするタイミングがいくつかあります。

  • (a) ソースコードからのパース時点で【改行】として扱う。
  • (b) HTML出力時に改行コードを見つけたら直接 <br> に変換する。

とはいえ、(b)の手段はatsphinx-linebreakとやっていることはそこまで変わりません。
ここでは(a)について少しだけ考察してみます。

Sphinx単体でMarkdownを扱うことはできず、MyST-Parserという「Markdownをdoctreeに変換する」ための拡張を必要としています。
実はMyST-Parserは内部のMarkdownをパースする処理のために、markdown-itのPython実装を使用しています。

この記事を読んでいると一度だけ目にしたことがあるでしょう。そう、Zennの記事ソースのパースにもNode.js版の同名のライブラリが採用されています。
前出の通り、markdown-itは「改行コードを【改行】として処理する機能」を持っています。
つまり、本来であればMyST-Parserが設定をmarkdown-itに引き渡してさえくれれば、atsphinx-linebreakは無くても平気だったりします。
(とはいえ、実際問題として対応していないために拡張が誕生しました)

脚注
  1. 仕様が書かれている https://github.github.com/gfm/ 内を検索してもparagraph,lineはマッチするのに対してsentenceはマッチしません。 ↩︎

Discussion