kmuto’s blog

はてな社でMackerel CREをやっています。料理と旅行といろんなIT技術

コードハイライト付きの書籍をこれまでずっと作っていた

blog.jxck.io

で(md2inao→md2indesignの進行は過去にもちょっとかかわりがあってウォッチした)

もうすでにそういう製品があったり、知らないだけで全コードがハイライトされた書籍を出してる出版社はあるのかもしれないが、そういう本を少なくとも自分は見てない。

という記載があったのでちょっと書いてみる。

オーム社さん、オライリー・ジャパンさん、インプレスさん、羊土社さん、講談社サイエンティフィク社さんなどの一部の書籍では、コードハイライト付きになっていて、さらにそのうちいくつかは紙版では白黒、電子版ではカラーを使い分けていたりする。

というのも、前職の制作会社時代に私がその仕組みを作ってきたから。

組版InDesignを使うのもあれば、TeXを使っているのもある。紙白黒/電子カラーのような使い分けは、TeXではOK、InDesignではもしデータを2種類管理しなければならない場合はお断わりしていた(単純な白黒化であれば機械的にできるが、白黒時に見にくくなる箇所を調整うんぬんとなると、データを2種類持たなければいけなくなるので)。

『Reactハンズオンラーニング 第2版』(オライリー・ジャパン)より

コードハイライトにはlistingsかpygmentsを使っていた。

listings

古典的な自動コードハイライトとしてはTeXのlistings環境というのがある(日本語対応ラップがplistingsで基本的にはこちらを使っている)。オーム社の昔の本では、当時在籍されていた制作の方がこれを使っていた。

  • 手軽。TeXで組む前提で、かつデフォルト+キーワードをちょっと追加するくらいならこれで済むこともある。
  • プログラミング言語ごとのルール設定ができる。
  • TeX内で完結するのでビルドが速い。
  • 行番号や折り返しも併せて機能提供される。
  • だいぶ素朴なキーワードマッチなので、意図と違うハイライトをすることがある。しかし、処理途中に割り込むのは困難。
  • TeX以外で組むのには使えない。手作業InDesign組版の場合、組まれた結果をもとに目視手必死で手作業書体設定などをすることになる。

なお、Re:VIEWTeX用デフォルトテンプレートでも、ハイライト有効にしてlistingsを指定するとこれが使われる。

pygments

業務の大半ではpygments(およびRubyラッパーのpygments.rb)を使っていた。

  • わりとがんばってプログラミング言語をパースしてくれる。
  • ターミナル、HTML、TeXなど書き出しフォーマットを指定できる。
  • プログラミング言語ごとのルール設定ができる。
  • 割り込んでのカスタマイズがいろいろできる。
  • 外部プログラムとなるため、TeXではコンパイラ-shell-escape付きで呼び出さないといけなくてちょっと嫌。
  • パースが遅いので、高速化を工夫する必要がある。

pygmentsはPythonライブラリなのでpipでインストールする。変換はpygmentizeというコマンドを使う。プログラミング言語と出力フォーマットを指定すると、出力フォーマットに沿った形に変換してくれる。HTMLやLaTeXの場合はハイライトの書体や色を直接割り当てるのではなく、lexerで解析されたトークン名にしておいて、そのトークン名とスタイルとを結び付けるという形になっている。

$ cat test.js
const lordify = function(firstname) {
  return `${firstName} of Canterbury`;
};

ターミナル表示での変換(manniスタイル)

# HTML変換
$ pygmentize -l javascript -f html test.js
<div class="highlight"><pre><span></span><span class="kd">const</span><span class="w"> </span><span class="nx">lordify</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kd">function</span><span class="p">(</span><span class="nx">firstname</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">return</span><span class="w"> </span><span class="sb">`</span><span class="si">${</span><span class="nx">firstName</span><span class="si">}</span><span class="sb"> of Canterbury`</span><span class="p">;</span>
<span class="p">};</span>
</pre></div>

# HTMLのmanniスタイル
$ pygmentize -S manni -f html
pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
 …

# TeX変換
$ pygmentize -l javascript -f latex test.js
\begin{Verbatim}[commandchars=\\\{\}]
\PY{k+kd}{const}\PY{+w}{ }\PY{n+nx}{lordify}\PY{+w}{ }\PY{o}{=}\PY{+w}{ }\PY{k+kd}{function}\PY{p}{(}\PY{n+nx}{firstname}\PY{p}{)}\PY{+w}{ }\PY{p}{\PYZob{}}
\PY{+w}{  }\PY{k}{return}\PY{+w}{ }\PY{l+s+sb}{`}\PY{l+s+si}{\PYZdl{}\PYZob{}}\PY{n+nx}{firstName}\PY{l+s+si}{\PYZcb{}}\PY{l+s+sb}{ of Canterbury}\PY{l+s+sb}{`}\PY{p}{;}
\PY{p}{\PYZcb{}}\PY{p}{;}
\end{Verbatim}

# TeXのmanniスタイル
$ pygmentize -S manni -f latex
\makeatletter
\def\PY@reset{\let\PY@it=\relax \let\PY@bf=\relax%
    \let\PY@ul=\relax \let\PY@tc=\relax%
    \let\PY@bc=\relax \let\PY@ff=\relax}
\def\PY@tok#1{\csname PY@tok@#1\endcsname}
\def\PY@toks#1+{\ifx\relax#1\empty\else%
    \PY@tok{#1}\expandafter\PY@toks\fi}
\def\PY@do#1{\PY@bc{\PY@tc{\PY@ul{%
    \PY@it{\PY@bf{\PY@ff{#1}}}}}}}
\def\PY#1#2{\PY@reset\PY@toks#1+\relax+\PY@do{#2}}

\@namedef{PY@tok@w}{\def\PY@tc##1{\textcolor[rgb]{0.73,0.73,0.73}{##1}}}
\@namedef{PY@tok@c}{\let\PY@it=\textit\def\PY@tc##1{\textcolor[rgb]{0.00,0.60,1.00}{##1}}}
\@namedef{PY@tok@cp}{\def\PY@tc##1{\textcolor[rgb]{0.00,0.60,0.60}{##1}}}
\@namedef{PY@tok@cs}{\let\PY@bf=\textbf\let\PY@it=\textit\def\PY@tc##1{\textcolor[rgb]{0.00,0.60,1.00}{##1}}}
\@namedef{PY@tok@k}{\let\PY@bf=\textbf\def\PY@tc##1{\textcolor[rgb]{0.00,0.40,0.60}{##1}}}
\@namedef{PY@tok@kp}{\def\PY@tc##1{\textcolor[rgb]{0.00,0.40,0.60}{##1}}}
\@namedef{PY@tok@kt}{\let\PY@bf=\textbf\def\PY@tc##1{\textcolor[rgb]{0.00,0.47,0.53}{##1}}}
 …

InDesignの場合

InDesign組版する場合は、原稿のコード部を抽出してHTMLに変換し、InDesignの文字スタイルを割り当てるための情報とする。

自動組版的に作り込むならInDesignXML読み込みで反映されるようにHTMLタグを活用していくが、手動組版を前提とするときには、HTMLタグを■赤太■〜■/赤太■のような適当な代替文字にしておき、これをInDesign上に貼り付けてから、実際の文字スタイルに正規表現置換する方式を採っていた。これだとXMLで発生しがちなデータのゴミが残らなくて済むし、手動組版のオペレータには普通の組版指示と同程度なのでわかりやすい。

なお、InDesignではある箇所で割り当てられる文字スタイルは基本1つのみで、HTMLでいうところの「<span color="#FF0000"><b><i>RED</i></b></span>」という多段指定はできないので、登場可能性のあるぶんだけすべてスタイルを準備する必要がある。たとえば「赤」「太字」「斜体」「赤+太字」「赤+斜体」「太字+斜体」「赤+太字+斜体」といった具合で個別にスタイルを作る(スタイルの単一継承機能はあるので、色なり字形なりをベースに継承していくことは一応できるが)。

たとえば『リファクタリング(第2版)』などはInDesign XML自動組版で作ったけれども、これはハイライト回りの文字スタイルをいろいろ錬成した。

TeXの場合

TeX組版する場合は、内部でpygmentizeを呼び出すmintedというライブラリを使っている(外部呼び出しとなるため、-shell-escapeTeXコンパイラオプションに付ける必要がある)。

基本的にはこれでお任せではあるが、制作用途で処理に割り込みたいことがあるケースも多いので、\renewcommand{\MintedPygmentize}{ラッパースクリプト} を指定して、pygmentizeをラップするスクリプトで以下のような諸々の処理をしていた。

  • スタイルの色のRGB値→CMYK値変換
  • 白黒モード制作時の二値化
  • 絵文字などupLaTeXで普通に通すと壊れてしまう箇所の処理
  • 書籍によってはイタリック禁止処理
  • トークンの調整
  • 網掛けや番号付けなどコード本来にはないlexerで壊れてしまう箇所の手当て

オライリー・ジャパンさんの主に機械学習WebGLなどの本ではこの仕組みを使ってコードハイライトを実現していて、白黒本と電子カラー本を1つのソース(Re:VIEW管理)から作り分けている。『ゼロから作るDeep Learning』シリーズとか、『初めてのGo言語』『マスタリングLinuxシェルスクリプト』などなど。

Re:VIEW公式テンプレートでpygmentsをデフォルト有効にしていないのは、pygments周りを追加インストールする必要があるのと、-shell-escapeを付けないといけないことにデフォルト値としては若干のためらいがあることによる。SphinxではTeXソースに変換する時点でpygmentsを介してハイライト済みソースに書き換えておくことで、コンパイラ-shell-escapeなしに動くようになっているようだ。

Ruby実装のrougeもあるが、若干パース結果に差異があるので、旧来互換性のためにpygmentsを使い続けている。ただrougeにすれば最小限の依存関係でSphixと同じような実装方法もとれそうな気はするので、試してみてもいいかもしれない。

ハイライトとエスケープ、インライン指示

ハイライト自体はInDesignにせよTeXにせよそう大きな問題ではないが、実際の制作では別のいろいろな要求で頭を悩ませることも多い。

たとえば、著者原稿環境でよくあるGitHubVS Code、Jupyterはそれぞれ異なるハイライトルールが用いられており、pygmentsとは完全に合致しないことも多い。そのため「著者原稿のハイライトカラーと完全一致させるように」という条件が課されている場合(著者がそこまでの気持ちがなくても出版社としては判断を避けたいということはままある)、pygmentsのルールを作り込んだり、変換結果をフックして必死書き換えをしたりということも発生する。

過去にはlistingsっぽいもの、VS Codeっぽいもの、geditっぽいもの、Google Colabっぽいもの、のスタイルを作った。トークナイザーにまで踏み込まないと無理そうというときにはコスト的に諦めていただくこともある。

github.com

また、太字・網掛けや引き出し線コメントといった指示が原稿内にあると、lexerと競合することになる。これについては、変換した後に書き換えたり、あるいはいったんそのプログラミング言語のコメントにして退避しておきやはり後で書き換えたり、といった場当たりな手段を採ることが多い。mintedであれば数式モードを無理矢理使う手もなくはないが、不幸なことが起きそうな予感がするので、たぶんやめておいたほうがいいだろう。

白黒でコードハイライトは本当に見やすいか

制作していて気になっていたこととしては、特に白黒紙面において、太さを変えたり薄くしたり斜体にしたりするのは、本当に読者の読みやすさに貢献するのだろうか?というのがある。

読者が入力すべき箇所、あるいは説明上強調したい箇所について太字なり下線なりを付けるのは妥当という確信があるが(そしてこれはコードハイライトとは関係が薄く、昔から実施していること)、カラーのエディタ画面を前提として表現されていた情報を白黒に潰れた状態で劣化表現することで、たとえばJavaScriptのキーワードが太字化されているということで、何か読者に価値を提供できるのだろうか。

WEB+DB PRESS休刊発表に寄せて

WEB+DB PRESSについては昨年から定期購読を始めて業務勉強に役立てようとしていただけに、残念には感じつつ、数々の雑誌の休刊を見てきた中でよくがんばったな……という思いがあります。紙をはじめとする昨今の制作費の高騰は相当大変だったろうし(これは出版社の皆さん共通意見ですね)、とはいえ電子版の制作に舵を切るには採算面など壁が高かったのだろうと推察します。

編集部の皆さまにおかれましては、寄稿こそできなかったもののかかわりのある方々も多かったので、本当にありがとうございました。Software Designやほかの場面でまたご一緒できればと思います。