LaTeXでコードブロックの長い行を折り返せるfvextraを手なずける
(TeX & LaTeX Advent Calendar 2024の10日めの記事です。9日めはCareleSmith9さんによるhm Latain Modern—フォントをいじった話でした。)
2024年現在、LaTeXでコードブロックを整形するときの最適な選択肢はfvextra
パッケージでしょう。以下のような特長があります。
- コードブロック中で各種のコマンドが使える
- コードブロック中での空白文字や行の見せ方を簡単に調整できる
- 構文ハイライトの機能が組み込まれていない(これはメリットです)
- コードブロック中で長い行の折り返しができる
書籍のようなページメディアでは、長い行の自動折り返し機能は特にありがたい。しかしこのfvextra
の行折り返し機能、わりとデフォルトだと「そうじゃない!」という結果になりがちで、あまりうまく活用されていない気がします。
というわけで、本稿ではfvextra
の行折り返し機能を手なずけていきます。
1行が長いコードの例
まずは以降の説明で使う長い行のコード例として以下のようなJavaのコードを組版することを考えます。
String result = new BufferedReader(new InputStreamReader(new URL("https://example.com").openStream())).lines().collect(Collectors.joining("\n"));
「こんな書き方するかよ!」と言いたくなるかもしれません。コードの意図を説明することが目的の場合は編集部でうまい感じの改行とインデントを施すのですが、出力メッセージ中とかにこういうコードが出てくる場合もあったりするので、まあまあ原稿中に出てきてもおかしくない事例だと思います。
このコードをfvextra
パッケージのVerbatim
環境でふつうに組んでみましょう。
\documentclass{article}
\usepackage[showframe]{geometry}
\geometry{
paperheight=7\baselineskip,
paperwidth=20cm,
left=1cm,
right=5cm,
nohead,
nofoot,
marginpar=0pt,
marginparsep=0pt,
heightrounded
}
\usepackage{fvextra}
\begin{document}
\begin{Verbatim}
String result = new BufferedReader(new InputStreamReader(new URL("https://example.com").openStream())).lines().collect(Collectors.joining("\n"));
\end{Verbatim}
\end{document}
とくに改行もされず、順当にオーバーフローしますね。なお、オーバーフローしてるようすを伝えるために、本稿のコードではgeometry
パッケージの機能で版面に枠を描いてあります(以降ではgeometry
の設定は再掲しません)。
長い行の折り返し機能はbreaklines
オプションで有効にできます。
\begin{Verbatim}[breaklines]
String result = new BufferedReader(new InputStreamReader(new URL("https://example.com").openStream())).lines().collect(Collectors.joining("\n"));
\end{Verbatim}
どうやら空白文字の位置で改行されたようですね。自動的に改行された行の先頭に「折り返されてる」ことを示すマークっぽいものも出力されています。しかし、空白文字がない後半は、やはりオーバーフローしていますね。1行めと2行めもなんか間抜けっぽい。
空白文字以外でも改行されるようにするには、breakanywhere
というオプションも指定しなければなりません。
\begin{Verbatim}[%
breaklines,
breakanywhere
]
String result = new BufferedReader(new InputStreamReader(new URL("https://example.com").openStream())).lines().collect(Collectors.joining("\n"));
\end{Verbatim}
ようやくオーバーフローしている行がなくなりました。しかし、満足にはほど遠いと思います。まともな感覚であれば、以下をなんとかしたいと思うでしょう。
- 1行めの改行位置は、空白文字の前後が優先されているようだけど、そんな必要ないのに…
- 2行めの末尾には「次の行へ続く」ことを示すマークが出ているっぽいのが、1行めには出てない。なぜ…
- 2行めと3行めの冒頭に出ている「折り返されている」ことを示すマークの後に余白いらない…
- そもそもマークがカッコ悪い…
全般にfvextra
パッケージによる自動的な行折り返しの結果は、そのままだと「ぱっと見」で自動的な折り返しがされているように見えにくいと思っています。これをなんとかするにはどうしたらいいか、というのが本題です。
空白が改行位置として優先されないようにする
オプションの名前がbreakanywhere
なのに引き続き空白文字が優先されてしまうのが悪いと思うんですが、どうもfvextra
の大前提として「空白文字では改行してもいいはず!」というノリがあるようです。
この空白文字に対する優先概念を抑制するにはbreakcollapsespaces=false
を指定します。
\begin{Verbatim}[%
breaklines,
breakanywhere,
breakcollapsespaces=false
]
String result = new BufferedReader(new InputStreamReader(new URL("https://example.com").openStream())).lines().collect(Collectors.joining("\n"));
\end{Verbatim}
だいぶ「自動改行」っぽく見えるようになってきました。
「次の行へ続く」マークが出たり出なかったりするのをなんとかしたい
breakcollapsespaces=false
を指定すると、たまたま空白位置で自動改行された場合には「次の行へ続く」マークが出なくなるはずです。これを直接なんとかする方法は判明していません。しかしドキュメントを読んでいると以下のような事実に気づきます。
- いっそ「何も出さない」ことは可能(
breakanywheresymbolpre
の設定を無で上書きする。デフォルトで表示されるのは、このオプションに指定されている記号) - 「とにかく折り返した行の末尾はマークを出す」という方針を採用することもできる(
breaksymbolright
というオプションに出力したいマークを指定する。デフォルトでは無) - 左側に出力される「折り返されている」マークについては、これとは逆に
breaksymbolleft
のほうがデフォルトで設定されていて、breakanywheresymbolpost
は無になっている
要するにデフォルトは「空白文字の前後はみんな自動で改行してほしいよね!」というおせっかいな方針なので、これらの設定がねじれているわけです。breakcollapsespaces=false
にするということは、その逆にしておけば解決するので、下記のようにすれば「いつも行末に出る」ようになります。
\begin{Verbatim}[%
breaklines,
breakanywhere,
breakcollapsespaces=false
breaksymbolleft={\contline},
breaksymbolright={\tonextline},
breakanywheresymbolpre={},
]
String result = new BufferedReader(new InputStreamReader(new URL("https://example.com").openStream())).lines().collect(Collectors.joining("\n"));
\end{Verbatim}
上記では左側に出力される「折り返されている」マークも再定義してかっこよくしています。
\contline
とか\tonextline
という記号は存在しないので各自で好きなものを定義しよう!
マークの前後に余白が入らないようにする
ここまでの設定でだいぶましになりましたが、breaksymbolright
を定義するとなぜか行末に余白が入るようになってしまいました。よく見ると同じような余白は前方の「折り返されている」ことを示すマークの後ろにもこれまでずっと入っています。これいらない。
これらは、前方がbreaksymbolsepleft
で、後方はbreaksymbolsepright
で設定します。さらにbreaksymbolindentright
を使うと、右側版面いっぱいまでコードを使う(つまり右側のマークを版面の外に出す)が実現できます。
\begin{Verbatim}[%
breaklines,
breakanywhere,
breakcollapsespaces=false,
breaksymbolleft={\contline},
breaksymbolright={\tonextline},
breakanywheresymbolpre={},
breaksymbolsepright=0pt,
breaksymbolsepleft=0pt,
breaksymbolindentright=0pt,
]
String result = new BufferedReader(new InputStreamReader(new URL("https://example.com").openStream())).lines().collect(Collectors.joining("\n"));
\end{Verbatim}
装飾の中でも自動改行する
fvextra
パッケージの特長に、コードブロック中で任意のLaTeXコマンドを実行できるというものがあります。これによりキーワードの構文ハイライトなどが柔軟に実現できるわけですが、これが自動改行と相性が悪い。コマンド内ではbreakanywhere
が効かないからです。
たとえばJavaのnew
キーワードを\textbf
で太字にしてみると、こうなってしまう(Computer Modern Typewriterの太字はlmodern
で出せるよ)。
これに対処するにはbreaknonspaceingroup
というオプションをさらに指定します。
しかし、実はこのオプションで対処できるのは「1引数のコマンド」だけで、たとえば色を付けたいと思って{\color{red}\textbf{new}}
などとすると破綻します。意図通りの結果にならないどころか、エラーになってしまうでしょう。
この問題については\FancyVerbBreakStart
および\FancyVerbBreakStop
という低水準のインターフェイスが用意されていて、たとえば下記のようなコマンドを作ってそれを使うことにより解決できます。詳しくはfvextra
のドキュメントを参照。
\newcommand{\javaKeyword}[1]{%
{\color{red}\bfseries\ttfamily\FancyVerbBreakStart #1\FancyVerbBreakStop}}
まとめ
fvextra
パッケージには、verbatimな環境における長い行の自動折り返しという便利な機能があるが、デフォルトのノリがなんかちょっと違うので、素で使うとがっかりするかもしれません。しかし大量のオプションが用意されていて、それを見ながらカスタマイズはできるので、がんばりましょう。とはいっても、ここに紹介したもの以外で使う機会があるのは、左側のインデントを無効にするためのbreakautoindent
くらいな気もします(コードよりもエラー画面を組版するときとかに便利)。
最後に、全体の最終形を貼っておきます。
\documentclass{article}
\usepackage[showframe]{geometry}
\geometry{
paperheight=7\baselineskip,
paperwidth=17cm,
left=1cm,
right=5cm,
nohead,
nofoot,
marginpar=0pt,
marginparsep=0pt,
heightrounded
}
\usepackage{graphicx, xcolor}
\usepackage{lmodern}
\usepackage{amsmath, amssymb}
\def\tonextline{\raisebox{1ex}[0ex][0ex]{\rotatebox{250}{\ensuremath{\boldsymbol{\curvearrowright}}}}}
\def\contline{\raisebox{0.2ex}[0ex][0ex]{\rotatebox{110}{\ensuremath{\boldsymbol{\curvearrowleft}}}}}
\newcommand{\javaKeyword}[1]{{\color{red}\bfseries\ttfamily\FancyVerbBreakStart #1\FancyVerbBreakStop}}
\usepackage{fvextra}
\begin{document}\pagestyle{empty}
\begin{Verbatim}[commandchars=\\\{\},%
breaklines,
breakanywhere,
%breakautoindent=false,
%breakpreferspaces=false,
breakcollapsespaces=false,
breaksymbolleft={\contline},
breaksymbolright={\tonextline},
breakanywheresymbolpre={},
breaksymbolsepright=0pt,
breaksymbolsepleft=0pt,
breaksymbolindentright=0pt,
breaknonspaceingroup,
]
String result = \javaKeyword{new} BufferedReader(\javaKeyword{new} InputStreamReader(\javaKeyword{new} URL("https://example.com").openStream())).lines().collect(Collectors.joining("\textbackslash{}n"));
\end{Verbatim}
\end{document}
Discussion