golden-luckyの日記

ツイッターより長くなるやつ

2020年 年賀状

2年ぶりにPostScriptしました。

github.com

コードを見てもわかりにくいかもだけど、「20をランダムに並べた配列を作り、 2020 という文字列が完成した場合にだけ色をつける」というロジックとしては単純なプログラムをPostScriptで手書きしています。

スタックに対する操作でプログラムを書くことで名高いPostScriptですが、データの格納にはarrayも使えて、これの操作にはわりとコツがいる、ということがわかりました(具体的には、put 操作をしても結果の配列がスタックに載らないので、 dupなどしておかないといけない)。

f:id:golden-lucky:20200101212945p:plain

出版社を作って4年が経ちました

ラムダノートという出版社を作って4年が経ちました。

https://cdn.shopify.com/s/files/1/1634/7169/files/132x320_240x.png

www.lambdanote.com

去年に引き続き、今年もちょっとふりかえりをしてみます。 この記事はラムダノートの技術 Advent Calendar 2019の25日めのエントリーです。

第4期(2018年12月~2019年11月)のふりかえり

『n月刊ラムダノート』はじめました

今年は『n月刊ラムダノート』という不定期刊行誌を3月に発行し始めました。 去年のふりかえりで第4期の目標として掲げていた「単発の本じゃない形で濃い技術情報をお届けする新企画」は、これのことです。

ぶっちゃけ、技術書、読むの大変じゃないですか? 正直なところ、作るほうもかなり大変です。 技術書に限らず、いま出版社が次々に新刊を出しているのは、半ば商売を維持するためという構造的な側面があります(それだけが理由ではないです)。 読む人も作る人もさまざまな無理感を抱いているなかで、それに飲まれるのはつらい。

そもそも、技術書を読んだり作ったりするというのは、そんな悲壮感が漂う行為ではなかったはずです。 実際、同じように読む人も作る人も大変という状況にあっても、その限界な状況をお互いが楽しめる場もあるのだと知りました。 いわゆる技術書典というやつです。 そこで感じた「こういうのでいいんだよこういうので」に対する技術書を作ってきた側としての試みが『n月刊ラムダノート』だとも言えます。

そもそも執筆してくれる人を募るところから不安でいっぱいでしたが、ありがたいことにエッジな方々から記事を寄せていただけて、1年めのVol.1は「No.3」まで出すことができました! こうやって一覧するだけでどれも読みたくなりますね。

Vol.1, No.3(ワインレッド)
  1. 代数的データ型とパターンマッチの基礎(κeen)
  2. パターンマッチ in Ruby(辻本和樹)
Vol.1, No.2(ミカンオレンジ)
  1. LISP 1.5の風景(川合史朗
  2. 計算機科学から見たディープラーニング(今井健男)
  3. Q#で始める量子プログラミング(田中孝佳)
Vol.1, No.1(セルリアンブルー)
  1. TCPの再送制御機構(西田佳史)
  2. 「コルーチン」とは何だったのか?(遠藤侑介)
  3. MLOps の歩き方(有賀康顕)

いまは次号に向けた準備をしています。 完全にぼくがボトルネックな状態になっており、すでに原稿を頂いている皆様には本当に申し訳ありませんが、引き続きよろしくお願いいたします 🙇‍♀️

とはいえ、企画が余っているという状態ではまったくないので、すでに執筆をお願いしている方、寄稿したら面白そうなネタがあるという方、寄稿させたい人がいるから紹介するという方、次の次の号あたりの掲載を目指してぜひご連絡ください。

www.lambdanote.com

なお、『n月刊ラムダノート』は現在のところ自社サイト直販のみ送料無料という販売方法をとっており、書店での販売はしていません。 が、これはテクニカルな問題によるもので、この販売方法にこだわっているというわけでもなく、もし取り扱いたいという書店さまがいたらご相談ください。 今期はいろいろ模索していきたいと思います。

あと、いちばん重要なことですが、紙版は在庫いっぱいなのでぜひお早めにご注文ください。 宅配便ではなくポスト投函ということもあって年末年始のお届けはもう無理なのですが、「お正月には電子版で読んで紙は年始に届いたのを会社にもっていく」とかどうでしょう!

技術書典7に落ちた

落ちるんですね…。 その日だけ半ば思いつきで電子版半額セールをしたのですが、予想を大きく超える数の注文を頂きました。本当にありがとうございました。 この規模の割引セールをする余裕はあまりないのですが、これを機に当社の存在を知ってもらえたような気もするので、率直にうれしかったです。

個人的には、セール中に技術書典7の運営のお手伝いをしていたので、サーバ(ShopifyでなくPDFを生成配信してるほう)が落ちないかはらはらだったよ…。

現金が足りなくなりそうになった

落選セールの売り上げがなかったらまじでまずかった。 綱渡りが続きます。

新刊は『プログラミングHaskell 第2版』

いろいろあって(主に訳者の @kazu_yamamoto さんのおかげ)、当社から「第2版」を出せました!

プログラミングHaskell 第2版(紙書籍+電子書籍)www.lambdanote.com

この本には個人的にはやはり思い入れがあり(前の会社で携わった本にはどれも思い入れがありますが)、とくに「改良したい」と思っていたところを改良できてよかったです。 完全に新しい本になっているので、旧版を持っている方にも価値があると思います(旧版の知識だけで現在のHaskellの型クラスを理解するのは厳しいと思う)。

ただ、『n月刊ラムダノート』をがんばっていたのもあり、第4期の書籍新刊はこの1タイトルのみでした。 このへんが経営的な課題としてあるのかもしれませんが、新刊をたくさん出さなくても生きのこる出版社を目指します。

第5期(2019年12月~)

そんなわけでラムダノートは第4期をなんとか生き延び、2019年12月から第5期に入りました。 年初にいきなり役員借入金(経営者から会社への資金貸し出し)が発生するらしい(白目)。 ただ、こちらのうかつなツイートに対してほんとにたくさんの応援を頂き、幸いにも必要な金額はだいぶ少額になりました。ありがとうございました!

最後に、来期は「いろいろな意味でうちでしか作れないであろう本」をいくつか発表できそうです。 お楽しみに!

直販サイトを作って書籍を売ること

昨日までこのアドベントカレンダーでは、PDFの内部の話から始めて、XMLという構造化文書の話、Pandocで記法を変換する話、EPUBで本というパッケージを作る話というように、徐々にレイヤを上げてきました。今日と明日はさらにレイヤを上げて、出版社の立場の話で締めくくろうと思います。

現在、日本の出版事業の中心は、「版元」「取次」「書店」という3者(いわゆる業界三者)が担っています。 メーカーと小売りの間に卸しがいるという構造は特別なものではありませんが、業界三者がちょっとだけ他と違うところがあるとしたら、書店と版元との柔軟な直接取引が少なく、取次-書店間、取次-版元間での委託取引が中心になっていることです。 この構造を支えているひとつの柱は再販価格維持制度による書籍の定価販売なんですが、この構造のおかげで、日本はかなり書店の数が多い国であり続けました。 2000年代初頭には全国で2万店くらいの本屋さんがあったようです(参考)。 米国の書店数も同じころにやはり2万店くらいだったらしいので(参考)、ほんとに本屋さんいっぱいあったんだなあ、という気持ちです。 いまは12000店を下回っているようなので、この20年で6割くらい、たぶん平成の間に1万店以上の書店が全国から消えたことになりそうです。

書店が少なくなったということは、当然、本を売る場所が減っています。 その一方で、新刊の発行点数は、同じ期間に毎年最大で3%ずつくらいの増加傾向にあります(参考)。 売る場所が少なくなったのに、新しく売るものの数が増えているということで、いろいろ嫌な想像が働いてしまいますよね。 ただ、少なくともこれだけはいえると思います。 読者の立場で考えたとき、「ふらっと書店に出かけてそこでたまたま1冊の本に巡り合う」といった機会は、じりじりと得難い貴重な体験になってきているのです。

そんな中で新しい出版社を立ち上げることにした当社では、書籍の主な販売チャネルとして「自社運用の直販サイト」を前提にした事業計画を立てています。 本当は取次さん経由で書店店頭でも買ってもらいやすくしたいとは常々思っているのですが、営業の専任がいないこともあって、この直販サイトで読者の皆さんから注文を頂く、つまり「小売業」が事業の中核です。

幸い、直販サイトを自分で立ち上げて小売業をするのは本当に簡単な時代になりました。 いろいろな選択肢があると思いますが、当社はShopifyを採用しました。 直販サイトをオープンした直後の2017年3月にカナダ大使館で「日本初のShopifyミートアップ」に参加した記憶があるので、ちょうどShopifyが日本市場に本腰を入れようとしていたころでもあったみたいです。 最近は山手線の車内でもShopifyのCMを見かけるので、ちょっとどきどきしています。

Shopifyのよいところ

Shopifyを採用した最大の理由は、APIが公開されていて自分でアプリケーションサーバを作れることです。 当社では、電子書籍を売りっぱなしにするのでなくアップデート版の継続提供などもしたいと考えていたのですが、そういう仕組みを最初から持っている小売サイトのためのプラットフォームはあまりありません。 そのため、販売サイトのガワのデザインだけでなく必要なアプリまで自分たちで作れるShopifyが魅力的でした。 むしろガワのデザインをへんに凝るつもりはなかったので、デフォルトでそれなりに満足できるのがありがたかったです。

あと、これは使ってから気づいたのですが、バックオフィスのための管理画面がものすごくよくできています。 いまはぼく自身が発注作業をすることはなくて、バックオフィス業務は共同経営者にお任せなのですが、管理画面が玄人向けすぎるとそうもいかなかったはずです。

さらに、Amazon PayやGoogle Payに早期から対応してきたこと(Apple Payも利用できるけどトラブルが多いので停止中)、手数料がお値打ちなPayment Gatewayが利用可能になったこと(実質Stripeで手数料が安いのはまじでありがたい)、コンビニ振込のKOMOJUに対応していて現金指向のお客さんもサポートできること、振込サイクルが1週間なこと、クレカ詐欺の検知機能(ちょっとシビアすぎる気もするけど)など、書籍のように単価が低い商品で小売をやっている当社のような立場にとって切実な点で地味にありがたい設計になっているのも助かっています。

Shopifyにがんばってほしいところ

いろいろあるといえばあるんですが、チェックアウト画面のデザインがたまに崩壊するようなので直ってほしい(チェックアウトまわりは欧州対応のために画面構成をユーザがいじれない)。あと、APIタイムアウトがWeb経由だと厳しい(ぼくだけが使うものはCLIでいいんだけど)。そういう細かい点くらいで、大きな不便は思いつかない気がします。

できればこのままWebの雰囲気に十分なじんでいる出店者にとって使い勝手がよいサービスであり続けてほしいです。

謝辞

これまで当社のサイトでお買い物をしていただけたすべての皆さま、物流をお願いしている明和流通システムの皆さま、本当にありがとうございます。これからもよろしくお願いします。

Shopifyがないとやっていけないので、ShopifyとShopifyを支えているRubyおよびRailsの開発者の皆さまには本当にありがとうございます。

最後に、当社の直販サイトの使い勝手やワーディングに何らかの良さがあったとしたら、立ち上げ前から今に至るまで折に触れて @miwa719 さんと @m_seki さんから有益なテストとフィードバックを頂いているおかげです。あのチームすごい。

TeXの脚注をなんとかする

この記事はTeX & LaTeX Advent Calendar 2019の24日目の記事です。23日めはwtsnさんの記事でした。25日めは☃さんの記事です。

今年は3年ぶりにTUGに参加してきました。 TUGというのはTeX User Groupのことであり、TeX界隈の開発者とアドバンストな利用者からなる世界的なコミュニティです。 年に1回、地球のどこかで集まっていて、それもまたTUGと呼ばれています。 2013年には東京でも開かれました。

なお、今年はKnuthも参加しています。前回の参加は10年くらい前のことで、そのときはiTeXというネタ発表をしたことでも話題になりました。 今年は完全に聴衆として参加していたのですが、それでも圧倒的な存在感が圧倒的でした。

日本からはわたしを含めて5人が参加し、それぞれ思い思いのKnuth体験をしたようです。 わたしは圧倒的に圧倒されて距離をとっていたのですが、気が付いたら寺田さんたちが一緒に写真を撮っており、それに混ざっていたら、ちょっとだけお話をする機会が得られました。

実は、以前からKnuth本人に直接尋ねてみたかったことがあったのです。 それは、「Knuthの奥様は本当に脚注が嫌いなんだろうか?」ということでした。 というのも、"The TeXbook"の15章の末尾には次のような引用があって、これがずっと気になっていたからです。

Don't use footnotes in your books, Don.
--- JILL KNUTH (1962)

わたしは、一読者として脚注が好きです。 脚注が多い本はよくない、という一般論は理解しているのですが、これまで自分が本を読みながら、その内容を理解するにあたっては、脚注の存在に何度も助けられてきました。 リニアに読んで理解できるように編集されているのがベストというのは、ほんとその通りだと思います。 が、そもそも世の中はリニアに説明しやすいことばかりではなく、それで脚注をなくすことに腐心するくらいなら、むしろ脚注を濫用するくらいのほうがまし、とさえ思うのです。 本文で説明を完結させるのが前提として、すっきり書き上げた本文に脚注が追加されて解説に厚みが出ることは、読者としては歓迎すべきことではないだろうかと。

で、せっかくなのでKnuthに「奥様はほんとに脚注が嫌いだったの?」と質問してみました。 いま考えると唐突な質問でしたが、すぐに15章の引用のことを思い出してもらえて、「あの引用は本当に彼女が言ったことなんだ。けど、もちろん冗談としてね」と教えてもらえました。 まあ、おおむね予想の範囲内の回答だったといえますが、冗談であるという言質を得ることはできて安心しました。

さらに、「脚注をTeXに実装したということは、自分でも脚注を使うつもりはあったんですよね?」とも聞いてみました。 すると、「いままで4000ページくらい書いてきて、4つだけ脚注を使ったよ」と自慢されました。 どの本で使ったのかは聞き損ねたのですが、日本に帰ってから持っている本をぱらぱら見たところ、TAOCP 2edのVol.2に1つだけ見つけました。 ほかにKnuthの脚注を見つけた人がいたら教えてください(TeXブックで脚注の例として紹介されてるやつらは除く)。

TeXの脚注は出力されない場合がありますね

さて、その脚注なんですが、TeXには確かに脚注のための仕組みが実装されています。 しかし、この仕組みではあらゆるところに脚注を配置できません。 典型的に問題になるのは次のようなケースです。

\vbox{abc\footnote*{def}.} % この脚注は消える
\bye

LaTeXでも同様です。

\documentclass{article}
\begin{document}
\vbox{abc\footnote{def}.} % この脚注は消える
\end{document}

消えないまでも、脚注として期待される場所、つまりページの最下部に脚注が出ないという制限がある場合もあります。 「 minipage で箱の最下部に出てしまう脚注をページの最下部に出したい」というのはLaTeX初心者あるあるですね。 最近だと、 tcolorbox で同様の問題に苦労している人もいると思います。 対症療法として「\footnotemark\footnotetext を使う」というテクニックを知っている人は少なくないと思いますが、そもそもなんでこんな仕様になっているのでしょうか?

TeXはなぜ脚注を期待どおりに扱えないのだろう

TeXの脚注がいろいろアレなのは、TeXがページ組み立ての際に脚注を「特別な行」として扱うことが根本的な原因です。 大事なところなので、少しだけだけ詳しく説明します。

TeXは本文を組むとき、まず段落をいい感じの見た目になるように行分割します。 それから、各行を「メイン垂直リスト」(MVL)という場所にいったん集めます。 このMVLに行を集める処理のことを「ページビルダ」と呼びます。

いま、ある行に \footnote{...} が出てきたとしましょう。 ページビルダは、その脚注の中身を「特別な行」とみなし、MVLでは該当する行の直下に追加します。

f:id:golden-lucky:20191223231657p:plain

ちょっと紛らわしいのですが、ページビルダは最終的なページを作るわけではありません。 最終的なページは、ページビルダがMVLに集めた行からさらに1ページ分の材料を配置することにより作り上げられます。 この作り上げられた最終的なページは \box255 という箱に入れられます。 また、その際に「特別な行」は「insert」という特別な箱に入れられます。 もともと脚注だったやつであれば、この特別な箱は \box\footins です。 最後にこれらの箱をDVIなりPDFなりの1ページとして出力するのが「出力ルーチン」(OTR)という仕組みです。

f:id:golden-lucky:20191223231712p:plain

ここでポイントになるのは、 \vbox の中に \footnote が出てきてもページビルダがそれを「特別な行」として扱えない、という事実です。 それどころか、ページビルダは \vbox の中を覗きません。 つまり、そもそも脚注があることに気付かないのです。 というわけで、 \vbox の中に出てくる \footnote は、かなり早い段階でTeXからは「不可視」の存在になっているのです。

脚注をMVL上で可視にすればよい

原因が見えれば対処の方法も考えられるというものです。 具体的には、不可視の脚注をページビルダから見えるようにしてMVL上に載せる方法を考えます。

とはいえ、ページビルダはどうやっても \vbox の中は見ません。 なんとかして \footnote の中身を \vbox の外に出して上げる必要があります。

それを手動でやる方法が \footnotetext テクニックです。 このテクニックで実際にやっていることは、「その \vbox が配置されるページ」と同じページの材料としてMVLに載る場所(つまり本文のどこか)を人間が判断し、そこに脚注の中身を置く、という手作業だったわけです。

同じことを自動的にやる方法もあります。 いったん \vbox の中身をトークンリストという場所に逃がしておき、それを \vbox の直下で復元する、というマクロを設置するのです。 具体的にはこういう仕掛けを書きます。

\newtoks\mftn
\def\mfootnote#1{%
  \footnotemark
  \edef\@tempa{\the\mftn\noexpand\footnotetext[\the\c@footnote]}%
  \global\mftn\expandafter{\@tempa{#1}}}%
\def\mfootnoteout{%
  \the\mftn
  \global\mftn{}}

\begin{...}
  \let\footnote\mfootnote
  ...
\end{...}
\mfootnoteout

ページ分割する箱にも脚注を入れたい

トークンリストによる方法は、tcolorboxminipage など、 \vbox と同等な他の箱の中に脚注を配置するときの手法としてもだいたいうまくいきます。 しかし、breakable オプション付きの tcolorboxframed などで、箱の途中にページ分割が起きる場合にはうまくいきません。 箱が複数のページに分かれてしまうので、「その箱が配置されるページ」と同じページの材料としてMVLに載せる、という戦略がそもそも成り立たないからです。

筆者は昨年のTeX & LaTeXアドベントカレンダーにおいて、この問題に対する解決のアプローチがLuaTeXで得られることを示しました。

ただ、昨年の時点では脚注の高さを計算する部分の作りこみが甘く、ページの高さが紙面のサイズを越えてしまう場合がかなりありました。 今年はそれを作り込んだので、それをTeX Conf 2019で発表することを予定していました。 また、昨年のアドベントカレンダーでは時間切れになって説明をまったく書かなかったので、今日の記事で書いたような話をひととおり話すつもりでいました。

以下の図は改良版の yafootnote による tcolorbox への脚注の実例です。入れ子を含むかなり複雑な状況にも対応できているのがわかると思います(ちなみに入れ子の内側を breakable にすることは tcolorbox の制限でできないはず)。

f:id:golden-lucky:20191224000256p:plain

yafootnoteの実装について(読まなくてもよい)

というわけで、最後にこの yafootnote が何をしているのかを雑に説明しておきます。ほとんど開発メモです。

yafootnote は、LuaTeXのいくつかのコールバックを利用することで「ページビルダがやっているTeXの脚注のメカニズムをMVL以外でも模倣する」ものです。 具体的には、下記のように各種のコールバックを利用します。

  • post_linebreak_filter コールバックを利用して、「脚注の中身を特別な行として該当する行の下に移動する」
  • vpack_filter コールバックを利用して、「MVLに載らない特別な行の高さをゼロにする」
  • buildpage_filter コールバックを利用して、 「tcolorbox で分割される場合にページの高さを必要なだけ減らす」
  • pre_output_filter コールバックを利用して、「MVLの特別な行を \box\footins に移動する」

post_linebreak_filter コールバックで脚注を「特別な行」としてMVLに載せる際には、LuaTeXの「属性」の仕組みを使っています。 TeXマクロで脚注のコマンドを定義するとき、脚注の中身を個別のボックスに入れて、そのボックスに対して特別な属性を設定するという使い方です。 この属性を他のコールバックでも引っ掛けることで、ページビルダに任せずに脚注の材料を \box\footins に移動するようにしています。

buildpage_filter コールバックの使い道がちょっとわかりにくいんですが、これは、 tcolorbox が「ボックスの材料を \vsplit する際の高さ」を計算する際に、それを「脚注の高さ」分だけ減らすという処理に利用しています。 これを計算するためには tcolorbox/tcbbreakable.code.tex の一部にも改造が必要でした。 この「脚注の高さ」は、 tcolorbox による分割ではない通常のページ分割でも考慮する必要があり、それは出力ルーチンでやるしかないので、出力ルーチンにも改造が必要になります。

さらに、ページビルダの動作には、TeXの処理のさまざまなタイミングで非同期的に発生するという罠があります。 そのため、 tcolorbox による分割まで考慮した「脚注の高さ」がbuildpage_filter コールバック経由で計算され尽くすのは、実際にはページが完成した後になってしまいます。 これはいかんともしようがないので、実際の組版ではLuaTeXを2回実行することにしました。 1回めの実行では、各ページで最終的に必要になる「脚注の高さ」をすべて計算し、それを外部ファイルにいったん記録します。 そして2回めの実行で、その情報を使って実際のページを組み立てます。

なお、この手法はFrank MittelbachによるLuaTeXによるフロートの最適配置の研究からヒントを得ました。

まとめ

脚注大好き。

XMLからEPUBを作る

昨日までの話をふりかえってみます。

  • 構造化文書というと、どうしてもXMLタグの書式で構造を示すあの世界観が想起されやすい
  • しかし、書式はあくまでも記法にすぎないと思うことにして、構造のほうだけ抽象データ型でかっちり用意するという世界観もありうる。これはPandocやDocutilsでいちおう成功を見ている

このアドベントカレンダーでは、後者の世界観を「ライトウェイト構造化文書」と呼んでみました。 特にPandocの成功については、代数的データ型、パーサコンビネータ、パターンマッチを兼ね備えたHaskellに依拠する部分が大きかったのではないかなと個人的には思っています。

今日は、そんなHaskellEPUBを作ろう、という話です。 PandocにはEPUB Writerもありますが、痒い所に手が届く感じではなかったので、自分で生成器を作ることにしました。

HaskellXMLを扱う

EPUBの中身はXMLです。したがって、EPUBを作るにはXMLを扱う仕組みが必要です。

ここで、Pandoc構造のような抽象データ型とXMLの関係にちょっと思いを馳せてみます。 抽象データ型をXML的に観れば、あるセマンティクスをひとつ固定してその構造を代数的データ型で表したもの、といえます。 ということは、XMLを抽象データ型の観点で見れば、抽象データ型をさらに抽象したもの、といえるでしょう。 そのような対象を扱う手段として、HaskellにはArrowという仕組みがあります。 そして、このArrowを利用してXMLを扱うライブラリとして、HXTというものが使えます。

HXTそのものについては、時間の都合上、この記事では説明を省略します。 かなり昔に書いた記事がいまでも役に立つと思うので、使ってみようと思う人はそちらを参考にしてください。

HXTを使ってXMLからEPUBを作るアプリケーションを作りました

PandocにはEPUB Writerがあるので、たとえばMarkdownの原稿からEPUBを作るなら、素直に pandoc コマンドを使えば十分です。 しかし、野良XMLからEPUBへの変換でPandocを使うのは、個人的にはあまりメリットがないと考えています。 入力もXMLであるような場面で、わざわざその構造をPandoc構造にいったん潰してしまうのは、あまりうまくないからです。 原稿がXMLっぽいもの(HTMLを含む)なら、その構造を目いっぱい使ってEPUBを作りたいものです。

っていうふうに書くと、「EPUBの中身はXMLなのだから、元のXMLをそのまま使うだけではないのか」と思う人がいるかもしれません。 しかし、それだけだとふつうはEPUBにはなりません。 確かに、データとして見れば、EPUBは「XMLをZIPしたもの」です。 しかし、コンテンツとして見ると、Webブラウザなどで閲覧する前提で書かれたXML(あるいはHTML)と書籍として公開する前提のEPUBとでは、いろいろ異なる点があります。 そのためちょこちょこ細工が必要になるのです。

実際にEPUBを自分で作ってみると、面倒なのはこの辺の細工だなというのがわかると思います。

  1. ヘッダとか図のキャプションに文字列や連番を追加する
  2. そのヘッダに付加した連番で、本文から章や図を参照する
  3. 論理目次と物理目次をはじめとするメタ情報を作成する

1つめの話は、たとえば<h1>要素の頭には「第1章」、その下の<h2>要素の頭には「1.1」、図には「図1.1」などの文字列を連接するということです。 そんなのCSSでやればいいじゃん、と思うかもしれませんが、CSScontentプロパティが使える広義のEPUBリーダー(Kindleも含む)は一部なので、これはコンテンツのHTMLのほうに埋め込む必要がある、というのがEPUB作成者の間では一般的な認識だと思います。 また、そうやって生成した連番のテキストは、場合によってはそれを参照している側にも付加する必要があり、これが上記でいうと2つめの項目です。 これらの作業には、当然、(テキストではなく)XMLに対する操作が必要になります。

3つめの話は、読者が目に見える部分だけじゃなくて、そもそもEPUBの仕様的に必要なメタ情報がいっぱいあります。 詳しくは他の資料などを参照してもらうとして、とくにめんどくさいのはOPFファイルと呼ばれるものの準備です。 このファイルに、中で使われている画像から何からすべての情報をきちんと登録しなければ、正しいEPUBファイルになりません。 そのための情報は元のXMLソースから抽出してくることになり、これにもXMLの操作が必要になります。

先にもちらっとふれましたが、元のXMLをいったんPandoc構造に落としてもかまわなければ、Pandocがこのへんの面倒をすべてみてくれます。 Pandoc構造にはHTMLを生で保持するデータ構造もあるので、それで十分なことも多いでしょう。

が、せっかくなのでその辺の処理もいちど自分で経験してみるかと思って、HXTでXMLからEPUBを生成するアプリケーションを開発しました。 仕事でEPUBを生成する必要があるとき、いまでは基本的にこれを使っています(中身がわかっているので手をいれやすい)。

ちなみに「qnda」という名前の由来は画面をひっくり返すとわかると思います。

このqndaを使って、「WikipediaのエントリをEPUB化するWebサーバ」をScottyというHaskellの軽量Webフレームワークを使って書いたこともあります。 HTMLではなくMediaWikiをソースにしているので、qndaのもともとのコンセプトは完全に失われていますが…

複数のエントリ(デフォルトでは3つまで)をフォームに指定してSubmitすると、それぞれを章とするEPUBファイルが返ってくるような仕掛けです。

f:id:golden-lucky:20191223182625p:plain

生成されたEPUBはリーダーでこんな感じに読めます。

f:id:golden-lucky:20191223182656p:plain

EPUB、売ってないじゃん

というわけでHTMLに変換可能な原稿からは技術的にEPUBを自力で生成できる当社ですが、いまのところEPUBの商品は販売していません。 Amazon Kindleでも販売していないんですが、これには次のような理由(というか言い訳)があります。

現状、EPUBが真につらいのは、生成することよりも、リーダーごとの動作検証だと思います。 PDFでさえ特定のビューワーが思いもよらない動作をする場合があるのですが(とくにApple系)、これにEPUBリーダーでの動作検証をして販売できるモノを準備する余裕がありません。 また、もしモノは用意できたとしても、管理する商品形態が1つ増えるのは当社の体力的につらく、そのために残念ながら見送っているのが実情です。 EPUBも欲しいという声があるのは承知しているのですが…。(そもそもEPUBに未来はあるのでしょうか…)

ちなみにKindleも事情は同じで、生成はできても、販売するために解決しないといけないバックオフィス上の課題がいくつかあります。

以上、ドキュメント屋さん的にはいろいろ面白いEPUBだけど出版社的にはちょっぴり難しい判断がある、という話でした。

明日の記事の予定はまだ決まっていません。

抽象データ型を自作する

昨日の記事では「書籍のマクロな構造」について話しました。 このマクロ構造はPandoc構造には組み込まれていません。 そのため、Pandocで書籍を作ろうと思うと、どうしたってPandoc構造にない部分を扱う別の仕組みが必要になります。 素のPandocでは、「書籍のマクロな構造を扱える外部の仕組み」を託す先として、主にLaTeXを利用しています。

裏を返すと、LaTeXは、書籍のマクロな構造を扱える仕組みです。 それなら最初からPandocではなく、LaTeXで本を作ればいいのではないでしょうか?

この反論はもっともです。 実際、本を作るプロは黙ってLaTeXであったり、あるいはInDesignであったり、あるいはFrameMakerであったりを使います。 組版のプロの要求を実現するためには、これらのツールが持つ表現への自由度が必要だからです。

しかし原稿をもらう立場からすると、この高い自由度がかえって仇になるケースのほうが多い。 原稿の肝の部分はPandoc構造くらいに制限されたセマンティクスで書いてもらえるほうがよい。 そのようなわけで当社では、「Pandocで処理をすることを前提としたMarkdown」を、原稿の執筆のための最初の記法として選択することにしました。 Pandoc構造は、最大公約数的な意味で、とてもうまくできていると思います。

なお、LaTeXに関していうともうひとつ難点があって、それは「LaTeX原稿は再利用が容易とはいえない」ことです。 これは、「Markdownのほうが再利用しやすい」という意味ではないので注意してください。 再利用しやすいものがあるとしたら、Markdownのような記法ではなく、そのセマンティックな構造を直接表した抽象データ型のほうです。 Pandocが多様な形式のドキュメントに対応できているのも、この抽象データ型の再利用の容易さの表れだといえるでしょう。

そんなわけで、「これから書く」ものに関してはPandoc構造を前提とした記法でいいと思います。 しかし、世の中には、「すでに書かれたものでPandoc構造では不足がある」ような原稿も当然あります。 標準的な処理系が利用できるLaTeXなどの原稿であればいいんですが、そうではなかったり、そもそも原稿の記法が未知であるようなケースもあります。 そうした原稿を手にした場合に、どうやってそれを手懐けるかというのが今日の話です。

結論からいうと、Pandocが採用しているアプローチ、つまり「記法から代数的データ型を読み取って、それを目的に合わせて出力する」という方針で挑みます。 具体的には、次の3つの仕組みを実装します。

  • その記法が表す構造を押し込めるのに都合がよさそうな抽象データ型の定義
  • それに合わせて記法から構造を読み取るReaderの実装
  • 抽象データ型に対する出力を定義したWriterの実装

Pandocの場合には、最初に意識されていた記法が「Markdownのそれ」だったといえるでしょう。 その実装であるPandoc構造よりもリッチな構造を読み取れそうな記法で書かれた原稿があったなら、その原稿を眺めながら自分で代数的データ型とそれをターゲットにしたReaderを実装し、自分が扱いやすいLaTeXやHTMLのような記法へと変換するWriterを書く、すなわち、自分専用のPandocを実装すればいい、というわけです。

未知の記法に向き合う

処理系が手に入らない未知の記法で書かれた原稿を手にする機会がある人は少ないと思うので、人口的ですが説明のための小さな例を用意します。

\chapter{Lorem Ipsum}

The quick \xdefn{brown fox}\emph{brown fox} jumps over the lazy dog (\ref{fig:fox}).

\bottomfloat \float{figure}
\include{fig1}
\caption{The fox}
\tag{fig:fox}
\endfloat

\section{Dolor sit amet}

Our \mono{etaoin shrdlu}.

\sidebar{consectetur adipiscing elit}
Ut enim ad minim veniam, quis nostrud exercitation.

Ullamco laboris nisi ut aliquip ex ea commodo consequat.
\endsidebar

\section{Excepteur sint occaecat cupidatat non proident, \titlenl; sunt in culpa qui officia}

\list{bullet}
\item Li Europan lingues

\item Lorem Ipsum
\endlist

これ、LaTeXに見えるかもしれませんが、LaTeXではありません。 ただ、TeXのアプリケーションではあります。 TeXというのは、いわば自分自身のオリジナルなドキュメントシステムを作るためのフレームワークで、その実装のひとつがLaTeXであり、LaTeX以外にもさまざまなフレームワークが存在します。 上記は、そのような「どこかのオリジナルなTeXベースのドキュメントシステム」の原稿のつもりです。 (なお、ConTeXtというLaTeXとは別のフレームワークかな、と思った人もいるかもしれませんが、それも違います。)

ここで選択肢は大きく2つあるでしょう。

  1. このTeXアプリケーションをTeX言語で実装する
  2. この原稿の記法をLaTeXに書き換える

具体的な仕様が公開されているTeXアプリケーションであれば、1もありえると思います。しかし、その望みが薄い場合、2のほうが無難でしょう。 とはいえ、この記法をどうやってLaTeXに書き換えるかは、それほど自明な問題ではありません。

ちなみに、この原稿に見えるTeXっぽいけどLaTeXにはないもの、たとえば\sidebarとか\list{bullet}TeXLaTeXの命令として定義してlatexコマンドで実行するという方針は、最悪です。 上記くらい単純だとそれも可能かもしれませんが、未定義のコントロールシーケンスっぽいやつを\defとかでアドホックに定義しても、全体の整合性はまずとれません。エラーを回避しながら何とかPDFにできたとしても、まともに組版できるとは限りません。

これはLaTeXに書き換えるという方針を採用する場合でも同じです。 たとえば、エディタの正規表現で原稿中のコントロールシーケンスっぽいやつをLaTeXのそれっぽいコマンドに変換していくだけでは、事態を悪化させる可能性が高いです。

そこで、Pandocのアプローチに倣って、この未知の記法から構造を取り出すことにします。 構造さえ取れれば、そこから先の工程をある程度は形式的に進められます。 何回か前の記事でPandocのアプローチのことを「ライトウェイト構造化文書」と表現しましたが、そこで含意していたのは、Pandocのアプローチにはこういう性質があるからだったのでした。

抽象データ型の定義

上記の記法を眺めつつ、必要そうなデータ型をHaskellで定義しましょう。 なんどかやっていると勘所がつかめてきます。 ブロックとインラインに分けるみたいな常套手段はあるんですが、結局は勘と経験だと思います。

この例の場合は、たぶんこんな感じに落ち着くと思います。

data MyBlock = Para [MyInline]
             | Header Int [MyInline]
             | FloatElem FloatType FloatPosition [MyBlock]
               Tag Caption [OuterResource]
             | ListItems ListType [([MyInline], [MyBlock])]
             | Sidebar [MyInline] [MyInline]
             | MyEmpty

data MyInline = TextString DT.Text
              | EmphString [MyInline]
              | TtString [MyInline]
              | Ref RefType Tag
              | Label Tag
              | Command DT.Text
              | CaptionCmd Caption

一発で決まることはまずないので、とりあえずデータ型を定義してみる→ReaderとWriterを作って結果と記法を見比べる→定義を追加したり修正したりする、の繰り返しになります。 というわけで、ReaderとWriterを作る話に移りましょう。

ReaderとWriterを作る

Readerは、ここまでの記事でなんどか登場しているParsecで作ります。 Parsecにおける「パーサ」とは、「局所的なパターンを見つけ、必要なデータ型としてreturnする機能をもった関数」のことです。 そういうパーサをいくつか組み合わせることで多機能なパーサを作り、「テキスト全体から構造を見つけてきてデータ型として吐き出す」というのがParsecのノリです。

たとえば、上記のうちブロック要素の構造を見つけてきてMyBlockを吐き出すようなパーサ(関数)は、こんな感じに複数の単機能のパーサの選択として定義できるでしょう。

myBlock :: Parser MyBlock
myBlock = choice
          [ try emptyLine
          , try myInclude
          , try myHeader
          , try myFloat
          , try myList
          , try mySidebar
          , myPara
          ]

emptyLine とか myPara みたいなのが単機能のパーサです。 先に定義した代数的データ型の各部に、各関数がそのまま対応していることが見て取れると思います。 このように代数的データ型との直観的な対応でパーサを定義できるのがParsec(というかパーサコンビネータ全般)の便利なところだと思います。

単機能のパーサは、原稿で記法がどう使われてるかを見て、それをそのまま定義として書き下します。 詳細は省きますが、 mySidebar であれば、「"\sidebar"という文字列に続いて波カッコで括られた文字列があり、そのあとにブロック要素が "\\endsidebar" という文字列が出てくるまで続く」ようなパターンの部分を見つけ出してきて、それを「Sidebar 型として return する」ような関数として定義する、という感じです。

mySidebar :: Parser MyBlock
mySidebar = do
  title <- string "\\sidebar" *> bracedMyInline <* spaces
  contents <- manyTill
              myBlock
              (try $ string "\\endsidebar")
  return $ Sidebar title contents

bracedMyInline = 
  string "{" *> manyTill 
    (try myInline <|> (TextString "\n" <$ (many (string " ") >> newline)))
    (try $ string "}")

Parsec、まじで強いので、テキストを処理するお仕事の人はこのためだけにHaskellを勉強しても損はしないと思います。

Writerを作る

WriterはReaderに比べると簡単で、構造に対する show メソッドを「LaTeXのソースを出力するもの」として定義してあげるだけです。

writeLatex :: [MyBlock] -> DT.Text
writeLatex myAST = DT.concat $ concatMap blockToText myAST

blockToText :: MyBlock -> [DT.Text]
blockToText (Para is) = "\n" : (map inlineToText is)
〈その他いろいろなブロック要素の出力方法をパターンマッチで定義〉

inlineToText :: MyInline -> DT.Text
inlineToText (TextString txt) = txt
〈その他いろいろなブロック要素の出力方法をパターンマッチで定義〉

実際には細かいところはいろいろ考えて実装しないといけないのですが、ノリとしてはこんな感じになるはずです。 時間と体力の都合により全体の実装は省略します。

まとめ

冒頭に提示した例は、実際に過去に自分が出会った「処理系が手に入らないTeXアプリケーション」の原稿を基にしています。 そのTeXアプリケーションはZzTeXというもので、英語圏の専門書の出版社でときどき採用されているようです。 調べたら、TUGboatに紹介記事がありました。

このときZzTeXの原稿をLaTeXへ変換するのに書いたプログラムでは、抽象データ型の定義だけで、最終的に全部で100行くらいの規模になりました。 今回の記事で例としてでっちあげた程度のドキュメントであれば、Pandoc構造に合わせてReaderを書くので十分かもしれません(足りない部分はDivとSpanでやる)。 しかし、これくらいの規模の構造を掘り起こさないといけない場合には、今回のような戦略で変換器を書くのがいちばん近道に思えます。

なお、この記事で「TeXアプリケーション」と呼んだものは、「TeXのフォーマット」とか「TeXのマクロパッケージ」とかって呼ぶほうが正確です。 マクロとかフォーマットというと、LaTeXをなんとなく使ったことがある人が知っているような気がする別のものが想起されやすいので、ここではTeX言語によるアプリケーションと表現することにしました。

明日はEPUBライターの話を予定していて、ぜんぜん話が変わるようですが、またHaskellを使います。

Markdownで書籍を作るとは

昨日まで何回かにわたり、多様なドキュメント形式の変換アプリケーションであるPandocのコアとなる仕組みを説明してきました。 特に、Pandoc構造とそれを生成するReader、生成されたPandoc構造を変換するPandocフィルターについて、少し時間をかけて紹介しました。

では、PandocのReaderとフィルターについて理解したところで、Pandocを使って本は作れるでしょうか?

いままでの説明には登場しませんでしたが、Pandocの出力側を担うWriterには、「PDF生成のためのLaTeX Writer」や「EPUB Writer」など、「本」を作るのに使えそうなものがあります。 それらWriterを制御するためのコマンドラインオプションはいろいろ用意されており、独自のテンプレートを指定することも可能です。

ただ正直なところ、これらのWriterは、吊るしの状態では売り物の本を作れるほどには洗練されていません。 本にはPandoc構造では表現できないより大きな構造があり、コマンドラインオプションやテンプレートでそれを完全に制御するのは難しいからです。 今日は、そのような書籍のマクロ構造を手なずける必要がある例として、書籍の前付けと本文とを区別する話をします。

Pandocで書籍のマクロ構造に対処する3つの方針

「ドキュメント」と一口にいっても、その用途に応じていろいろ形が決まっています。 その形を定める国際的な規格が成立しているものもあれば、長年の慣習でなんとなく定型ができているものもあります。 いわゆる書籍については、これといった公的な規格はありませんが、かなり長い年月にわたって受け継がれてきた形があり、それがマクロな構造を形成しているといえるでしょう。

ほとんどの日本の書籍は先頭付近に扉絵やクレジットがあり、序文などに続いて目次、それから本文、巻末にあとがきや参考文献、索引、奥付で終わるという構造になっています。 一方、Pandoc構造で与えられるのは、HeaderParaのようなブロック構造、あるいはそれらを構成するインライン構造だけです。 Pandocで本を作るときには、どうやってマクロな構造を組み立てればいいでしょうか?

さまざまな方針はあるでしょうが、おおまかには次の3つに区分できると思います。

  1. PandocのWriterもしくはフィルターでやる
  2. Pandocのメタデータとテンプレートでやる
  3. Pandocでやらない

1つめの方針は、昨日までの記事でだいたい想像がつくと思います。 マクロな構造をある程度決め打ちにし、必要があればフィルターで整えて、Writerで一気に出力するというものです。 EPUB Writerはこの方針だといえるでしょう。

2つめの方針は、LaTeXやConTeXtを介したPDF出力が該当します。

想像に難くないと思いますが、ぼくが採用しているのは3つめの方針です。 全体のワークフローを示すとこんな感じになります。

f:id:golden-lucky:20191221125932p:plain

マクロな構造は完全にLaTeXで用意しておき、PandocはあくまでもコンテンツをMarkdownからLaTeXの章や節に変換するのに使っています。 この方法の利点は、書籍のマクロ構造に関して完全に枯れた技術であるLaTeXのパワーを余すところなく利用できることです。 欠点は、Pandocで完結しないのでMakefileのような仕組みが必要になることです。 もっとも、この欠点は高い柔軟性という利点でもあります。

前付けの\section\section*にする3つの方針

書籍のマクロ構造についてPandocの側では気にせず、LaTeX側で面倒を見るという方針を取るのはいいとして、PandocでLaTeXへと変換したコンテンツが、そのままの形でLaTeX側で要求する書籍のマクロ構造の要件に合致するとは限りません。 たとえば、書籍では通常は章や節に連番を振りますが、前付けの「序文」などでは連番を振りたくありません。 この両者を区別するのに、LaTeXでは \section{...} コマンドと \section*{...} コマンドを使い分けます。 LaTeXで書籍を作るとき、つまり書籍のマクロ構造を組み立てるときには、「前付けでは見出しに \section*{...} を使い、本文では見出しに\section{...}を使う」という使い分けを、人間がやっています。 これをPandocの出力で切り替えることは、標準ではできないはずです(それともできたっけ?)。

なお、もし書籍のマクロ構造についてもPandocで組み立てる、という方針にしたならば、連番そのものをPandocに作らせるという手はあります。 その場合には、pandocコマンドに--number-sectionsを付けたり付けなかったり、あるいはヘッダに{.unnumbered}という属性を付けたり付けなかったりすることで、前付けと本文での \section の使い分けが可能です(実際には全部が \section* になる)。 しかし、こうしたマクロ構造はLaTeXの側で対処したいとなれば、別の方法を考える必要があるでしょう。

別の方法としては次の3つの方針がありえると思います。

  1. Pandocフィルターでなんとかする
  2. 出力された.texファイルに対してsedrubyの変換スクリプトを走らせる
  3. TeX言語でやる

投げやりですが、好きな方法で対応すればいいと思います。

まとめ

今日の記事で言いたかったことは、「Pandocは書籍を作るためのツールではないので書籍が簡単に作れるわけではない」ということの再確認です。 特に、書籍のマクロ構造については、そのためにPandocが裏で使っているツール(TeX/LaTeXなど)まで下りてくる必要があります。 Pandocのメタ情報などを駆使してある程度まではパッケージ化できると思いますが、完全な柔軟性がほしい場合もあるでしょう。

なお、Re:VIEWとかSphinxのような「本を作ることをもともと考えている仕組み」には、書籍のマクロ構造を扱う標準的な方法が用意されています。 それらをMarkdown原稿から利用するのもいいでしょう。 ただ、Re:VIEWにしろSphinxにしろ、その書籍のマクロ構造を扱う部分のデフォルトはLaTeXのエコシステムに対するラッパーなので、そのラッパーではカバーしきれない範囲については、やはりLaTeXに下りてくるしかありません。 さらに、LaTeXTeXのアプリケーションのひとつなので、LaTeXで不満な部分についてはTeX言語まで下りてくるしかない、というのはPandocと事情は同じです。

一口に「Markdownで本を書く」といっても、Pandocで可能な範囲でやる、LaTeXなどの仕組みまで下りてくる、TeX言語まで下りてくる、というふうに「自分がどこで何をやっているのか」を意識すると、どこでどういう対策が可能かの選択肢も見えてくるだろう、というのが今日の話のひとつのテーマでした。

明日は「未知の記法を前にして人はどうするか」という話をします。 以降はすべて余談です。

前付けの\section\section*相当にする(TeXで)

そろそろPandocフィルターの話も飽きてきたと思うので、最後におまけとしてTeXでやる方法を考えてみます。 最終的なコードは示しますが、これで常に使える出力が安全に得られるとは限らないと思うので、注意してください。

さて、こういう要求が生じた場合、まずはlatex.ltxを見ます。 そこでは\sectionがこんなふうに定義されています。

\def\section{\secdef\@section\@ssection}

番号あり見出しが\@section、番号なしの見出しが\@ssectionで定義されており、これをスター*の有無で切り替えるのが\secdefというマクロです。 ということは、\secdefの2つの引数の順番を逆にすれば、\section{...}\section*{...}の機能を切り替えられそうだな、とわかります。 いま組版してるのが本文なのかそれ以外なのかは\if@mainmatterで判別できるので、こんな感じに書けばよさそうですね。

% うごかない
\def\section{%
  \if@mainmatter
    \secdef\@ssubsection\@subsection
  \else
    \secdef\@subsection\@ssubsection
  \fi}

もちろんこれは動きません。それがなぜかを知るには、もう少しまじめにTeX言語を読むことになります。

とりあえず関連するマクロの定義をlatex.ltxから書き出してみます。

\def\secdef#1#2{\@ifstar{#2}{\@dblarg{#1}}}
\def\@ifstar#1{\@ifnextchar *{\@firstoftwo{#1}}}
\long\def\@ifnextchar#1#2#3{%
  \let\reserved@d=#1%
  \def\reserved@a{#2}%
  \def\reserved@b{#3}%
  \futurelet\@let@token\@ifnch}
\let\kernel@ifnextchar\@ifnextchar
\def\@ifnch{%
  \ifx\@let@token\@sptoken
    \let\reserved@c\@xifnch
  \else
    \ifx\@let@token\reserved@d
      \let\reserved@c\reserved@a
    \else
      \let\reserved@c\reserved@b
    \fi
  \fi
  \reserved@c}
\long\def\@firstoftwo#1#2{#1}

これらの定義に従って、\section*{...}の展開がどう進行するかを確認してみましょう。

\section*{}\secdef{\@section}{\@ssection}*{}
    ↓
\@ifstar{\@ssection}{\@dblarg{\@section}}*{}
    ↓
\@ifnextchar *{\@firstoftwo{\@ssection}}{\@dblarg{\@section}}*{}

ここまでは単純な定義による置き換えだけです。 ここで\@ifnextcharの定義により、次のような代入が起こります。

  • \reserved@d*
  • \reserved@a{\@firstoftwo{\@ssection}}
  • \reserved@b{\@dblarg{\@section}}
  • \@let@token*

最後のは\futureletによる代入で、これにより残されるのは\@ifnch*{あ}です。 そこで次は\@ifnch*{あ}の展開を考えます。

\@ifnchの定義をみると、\@let@tokenの値に応じた条件分岐になっています。 いま、\@let@tokenは「*」になっています。 一方、\@ifnextcharの定義による代入で、\reserved@d*になっています。 そのため、この条件分岐の結果として残るのは、\let\reserved@c\reserved@aの部分です。 これは次のような代入です。

  • \reserved@c\reserved@a

そのうえで\reserved@cが展開されます。 先の代入により、\reserved@a\@firstoftwo{\@ssubsection}になっているので、結果的に\reserved@cもそうなっています。

\@firstoftwo{\@ssection}*{}\@ssection{}

これでようやく、「 \section*{...}という命令で\@ssectionが選択されてるまでには、こんな具合に展開が起こる」ということがわかりました。

TeX言語のつらさは、ここで話が終わらないことです。 ここまでの流れを注意して読むと、\@ifstarの挙動を逆にするためには、\@ifstarに渡される1つめの引数の展開を先に済ませておく必要がある、ということに気付きます。 したがって、こんな感じに \section を定義し直してあげれば、スターの有無の挙動を本文以外では反転できることになるでしょう。

\def\section{%
  \edef\stared@section{%
    \if@mainmatter\noexpand\@dblarg{\noexpand\@section}\else
    \noexpand\@ssection\fi}
  \@ifstar{\@dblarg{\@section}}{\stared@section}}

繰り返しになりますが、これをコピペして使うことは推奨しないので、この話はこれで終わりです。

なお、こういう感じに「LaTeXレベルの挙動を変えるのにTeX言語をもってする」ことは、「TeX on LaTeX」と呼ばれています。 これは「Ruby on Rails」のもじりですが、単なるもじりでそう言っているわけではなく、実際にこのRuby on RailsにおけるRubyRailsの役割が、TeXおよびLaTeXにそっくりそのまま対応していることによります。

どういうことかというと、RailsRubyで作られたアプリケーションのひとつであり、そのRailsでしか通用しない話を、まるでRubyの機能のように説明することはできません。 逆に、せっかくRailsを採用したのに、そのやり方に逆らって素のRubyだけでWebアプリケーションを作ろうとするのは悪手です。 TeXLaTeXにも、それらの関係に近い、いやむしろほとんど同じ関係を言うことができます。 「TeX on LaTeX」という表現には、「LaTeXの文脈で済む話をわざわざTeXでやり直すのは悪手である」という気持ちがあります。

あるフレームワークでドキュメントを作るということは、そのフレームワークに備わっている機能の範囲でドキュメントを作るということです。 Pandocを使うということはPandocが想定している使い方を受け入れるということであり、LaTeXを使うということはLaTeXが想定している使い方を受け入れるということです。 その範囲に収まらないことをしたい場合にはHaskellなりTeX言語なりまで下りてくるしかない、というのが今日までの話のひとつの結論だったような気がします。