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