2026-06-04

株式会社 kubell を退職しました

はじめに

株式会社 kubell を 2026-05-29 (金) に退職しました。 2014-04-01 (火) に入社したので、12 年近く働いたことになります。

kubell は、ビジネスチャット・サービス Chatwork を開発・運営する会社です。 入社当時は ChatWork 株式会社という社名で、2018 年 11 月に Chatwork 株式会社 (Work の W の文字が小文字に)、2024 年 7 月に株式会社 kubell に社名を変更しました。 私はそこで iOS アプリエンジニアとして働いていました。

kubell 在籍中、印象深かったイベントがいくつもあります。 いくつかをピックアップしてふりかえることで、退職エントリーとします。 なお、本エントリーで触れているコードネームや内部事情については、会社ブログや YouTube で既に言及されているものばかりで、社外秘情報はありません。

2014-04 入社

2014-02-14 (金) は体験入社二日目。 技術部メンバーの前でプレゼンが予定されていました。 ところがその日は東京大雪。 まさかのリモート・プレゼン。

手元にあるのは小さな個人所有の MacBook が一台。 外付けディスプレイはなし。 画面共有を開始すると、ビデオ通話側の画面を隠さないとスペースが確保できません。 画面先のエンジニアの反応が全く分からない中、プレゼンを行い、運良く合格をもらうことができました。

入社初日の 2014-04-01 はなんとオフィス移転日。 池尻オフィスから合羽橋オフィスへの引越作業を、右も左も分からないまま手伝いました。 というわけで池尻オフィスで働いた (?) のはたった一日。 二日目からマシン設定等を始めて、私の新天地での仕事が始まりました。

2014-04〜2014-06 Eiger 開発

2014 年当時、Chatwork iOS アプリは Titanium SDK で実装されていました。 Titanium は JavaScript で iOS/Android のクロスプラットフォーム・アプリを作れる SDK です。

この手のクロスプラットフォーム開発用 SDK は、シンプルな画面を作るだけなら高速な開発成果が得られてとても良いものですが、iOS 特有の機能を使おうとしたり、少し手のこんだ仕様を実現しようとしたり、デバッグを行うとすると躓くポイントがたくさんあって、逆に開発スピードが遅くなってしまうことがありました。

Chatwork iOS アプリもその問題に直面し、Objective-C によるネイティブ実装に舵を切ります。 開発コードネームは Eiger。 アルプスの三大北壁の一つ、アイガー北壁を持つ山 Eiger です。 「難易度の高いアイガー (北壁) を攻略しよう」。 そんな意気も込めてのコードネーム設定だったと聞きます。

私はコードネームが決まって、開発が始まる直前にモバイルチームにジョインしました。

Chatwork iOS アプリの開発経緯などは、技術イベントで軽く触れられているので興味があれば見てみてください。

2014-05 シリコンバレー研修

iOS アプリ刷新プロジェクト推進の裏では、サーバーに刷新プロジェクトも動いていました。 イケイケです。 そんなことも追い風になったのでしょうか? 社員にシリコンバレーの雰囲気を知ってもらいたい、と言う話が出てシリコンバレー研修の予定が組まれました。

とは言っても社員全員が一勢に行くわけにもいかず。 いくつかのグループに分かれて向かうことになりました。 最初は○○チーム、3 番目は□□チームという具合です。

私たちモバイル開発チームは 2 番目に出発。 チームメンバーは私を含めて 4 人でした。

当時の ChatWork 社はアメリカに支社を持っていて、社長の山本敏行氏はシリコンバレー・オフィスを拠点にしていました。 時々日本には帰国していたものの、彼の主戦場はアメリカでした。 なので、私にとっては初めて社長に会う旅でもありました。

シリコンバレー研修。 一緒に行ったメンバー、ChatWork US 支社で会ったメンバー。 全員、もう辞めてしまいました。 ちょっと淋しいです。

2015-08〜2015-12 Scala を触る

先にも少し触れましたが、Eiger 開発と並んでサーバーの刷新プロジェクトも進んでいました。 PHP で書かれているサーバーのソースコードを、Scala で書き直そうというビッグ・プロジェクトです。 Eiger もこの Scala 版新サーバーで動くことを前提に実装が進みました。

Scala 選定に関しては、私の入社前に決まっていました。 開発合宿をやって決めたそうですよ。

そんな Scala サーバーですが、どうやら開発が遅延気味。 相談が持ちかけられたのは、たしか 2015 年 8 月の後半だったように思います。 上長がミーティングをしたい、と言ってきました。

しかし、オフィスのミーティングルームは全て埋まっています。 上長は大阪オフィス在籍で、なにかの用事で東京は合羽橋オフィスに来ていました。 つまり、大阪に戻る新幹線の時間があります。 悠長に会議室が空くまで待つわけにもいきません。

仕方がないので、近くの喫茶店で話すことになりました。 合羽橋に来たことがある方ならご存じと思いますが、合羽橋には喫茶店がほとんどありません。 そんな私たちの光明が喫茶店・合羽橋珈琲 (2016 年 8 月閉店)。 テーブルとテーブルの間が少し離れているので、ちょっとした小話程度なら良い感じにできるお店です。 ところが、その日に限ってお店は満席。 上長の新幹線の時間が迫ってきます。

8 月の日差しの中、トボトボ歩く二人。

道の先に見える自動販売機。

二人で缶コーヒーを買い、道の縁石に座って…

「サーバーが遅れてるんだよね。プッシュ通知回りの実装でヘルプに行ってもらえる?」

「モバイルチームで Scala を新しく学んでサーバーのコードを書けそうな人間、ぼくくらいしかいないですねぇ。行きます。」

それで Scala の最低限の勉強をして、サーバーのコードを書くことになりました。 あ、でも、もう 10 年近く前のことなので Scala の書き方はきれいさっぱり忘れてしまいましたね。

2016-01 サーバープロジェクト・キャンセル

サーバーの完成も近づいた 2016 年 1 月。 初めての iOS アプリとサーバーの疎通確認が行われました。

そして、上手くいかなかったんですね。

いろいろと手を尽くしたと思うのですが、無念のサーバープロジェクト・キャンセルとなりました。 サーバーのあれこれ顛末については、Podcast で放送されています。 もしお時間があれば、聞いてみてください。

サーバーキャンセル。

つまり、私が半年近く関わった Scala のコードも土に帰りました。

ショック。

開発を続けていればそういうこともあります。 一応、仕様とかフローチャート図とかは PHP の開発チームに引き継がれて実装が行われました。

2016-06 Eiger リリース

サーバープロジェクトはキャンセルされましたが、Eiger プロジェクトは止まりませんでした (ホッ)。

4 月末にはプッシュ通知回りの最終テストを実施。 バグが見つかって、ゴールデンウィーク返上でバグ修正。

iOS アプリ側でバグがないことを確認したので、PHP のソースコードを確認。 2 つのバグが相補的に不具合を引き起こしていたことを突きとめた時には、PHP エンジニアと一緒になって喜んだものです。

これが最後の大きなバグでした。 最終結合テストを経て 2016 年 6 月、ネイティブ版 iOS アプリがリリースされたのでした。

2016-12 メッセージID 枯渇問題

2016 年 12 月 29 日。 真夜中に 7 時間かけてサーバーのメンテナンスが行れました。

32 bit だったメッセージ ID の型を 64 bit に拡張するためのメンテナンスです。 このプロジェクトはサーバーチームが半年以上の入念な準備を整えて行われたものです。

なので、iOS エンジニアである私はほとんど関わっていません。 ただ、メンテナンス終了後の動作確認。 これは各クライアントチームが行う方が良い、ということで最後だけ参加することになったのでした。

東京在住のエンジニアで集まれる人は全員、当時の合羽橋オフィスに集合。 午前 0 時からの経過を見守り、少しトラブルはあったものの、最終的には無事リリース完了。 各クライアントチームが検証を行い、全てのテストをパスして、メンテナンスは終了となりました。

真夜中にオフィスに集まって、サーバーチーム・クライアントチームが混合で作業をした経験は、12 年の在籍期間中、後にも先にもこの一回限りです。 それだけに、とても印象深く記憶に残っています。

2017-11〜2018-05 Mauna Loa 開発

Eiger リリースでフルネイティブ化した Chatwork の iOS アプリですが、保守状況は必ずしも順風満帆というわけには行きませんでした。 その要因の一つは、サーバープロジェクト・キャンセルによる API 変更が上げられるかと思います。 Scala で用意された新 API を使う予定だったのに、急遽 PHP サーバーの旧 API を使うことになったため、Eiger 開発の終盤では腐敗防止層を導入して API の変換を行う仕組みを導入しました。 この腐敗防止層のおかげで Eiger リリースは最小の変更期間でリリースができたわけですが、その代わり新規開発への技術的負債になってしまいました。

折しも Swift 言語が iOS 業界でもメジャーになっていた頃。

Swift 化とリアーキテクチャーを行うプロジェクトが始まりました。 それが Mauna Loa。 Mauna Loa はハワイにある山の名前です。

Chatwork の iOS アプリには「山の名前」のコードネームを付ける、という伝統がありました。 ルールは一つ。 新しいコードネームには、より高い山の名前を付けること。

チームメンバーで Eiger より高い山をリストアップし、あれでもないこれでもないと話し合いをしました。 その中で持った共通認識が一つ。 Eiger より登りやすい山を選ぼう。

Eiger 北壁は難度が高すぎました。 Eiger の開発はその難度の高さから、iOS エンジニアの心を折っていたのです。

そこで選んだのが Eiger (標高 3,970 m) より 199 m 高いハワイの Mauna Loa (標高 4,169 m) でした。 画像検索ができるようなら是非 Mauna Loa を見て欲しいのですが、Eiger より高い山だというのになだらかな山だと思いませんか? これなら登れるよね?! という期待を込めて、コードネームが決定したのでした。

後日、Android エンジニアの方が Mauna Loa のアイコンを作って下さったのですが、なぜか Mauna Loa から溶岩が流れていました。 さすがに噴火中の Mauna Loa には登れませんよ? と思ったのはここだけの秘密です。

2018-11〜2021-04 AST プロジェクト

Chatwork の iOS アプリ開発プロジェクトの中で、「iOS 担当が私一人」なプロジェクトのうち「最長」の開発期間だったのが AST プロジェクトです。 2018 年末からコードを書き始めて、途中準備待ちとか別プロジェクトへの緊急ヘルプとかで開発が休止することもあったのですが、約 18 か月開発に費やしリリースにこぎつけました。

内容は Chatwork 内で使われる独自記法 (Chatwork 記法) を iOS・Android・Web で統一仕様にしようという試みです。

まず Chatwork 記法のパーサーを赤津氏が開発。

そして、パーサーが出力する AST (抽象構文木) に対して、タイムライン表示用の描画・メッセージ入力欄での編集用描画・コピー & ペーストの対応を私が行いました。 表示用の描画だけでコーディングに半年かかったのですが、その内 info トークンの描画実装についてはブログ記事に起こしています。

このプロジェクトを進めるに当たって、Chatwork 記法描画・編集・コピペの設計をほぼ一からやり直しました。

開発中、私は自分に一つ縛りを設けました。 それは「旧設計での描画」と「新設計での描画」の違いにユーザーがなるべく気づかないようにする、ということです。

Chatwork 記法の見た目の仕様を変えるプロジェクトではないので、ユーザーに気づかれないことが好ましい。 そんなちょっと変わったプロジェクトでした。

もちろんそれは表向きのことです。 細かな所に目を向ければ、行間が大きかったり小さかったりでガタガタした見た目が直っていたり、引用線が無駄に長かったのが短くなっていたり、絵文字の位置調整が正確になっていたり、長〜いメッセージへのケアが入っていたり、メッセージの範囲選択にあった諸々のバグが修正されていたり。 山のような改善と不具合修正が入っています。

言い換えると、「何が変わったのか分からないけど、何か使いやすくなった」と言ってもらえることを目指して開発しました。

リリース後、全くバズりませんでした。 狙い通りと言えば狙い通りなのですが、それはそれでモヤっとするのは私の器の小ささでしょうか?

2020〜 コロナとリモートワーク

2020 年にコロナが流行して、会社はフルリモートワークに舵を切りました。

この頃、kubell の社員数は 100 人を越えるくらい。 入社時点で 30 人程度しかいなかったこ とを考えると、社員数が急拡大していました。

採用するエンジニア層も即戦力の中途採用オンリーから、ジュニア・エンジニア、第二新卒、そして新卒受入れへと幅が広がりました。

結果、新入社員のサポート的な仕事も増えました。 と言ってもリモートワークが推進される時分です。 机を並べてサポートするわけにも行きません。

この頃、個人的な試みで成功したなぁと思ったのが、「Google Meet 繋ぎっぱなし」作戦です。

  1. 私が特定の Google Meet に入りっぱなしにしておく (マイクとカメラはオフ)
  2. 新入社員が質問なり相談なりしたくなったら、その Meet に入って声をかける
  3. 声が聞こえたら、私の方でマイクとカメラをオンにしてお話をする

こんなシンプルな仕組みです。

質問・相談があればチャットで投げれば良い。 文章コミュニケーションで足りなければ、改めて Google Meet で話せば十分。 という反論も聞くのですが、新入社員にはその最初のチャットを書くハードルが高い? 質問・相談内容が明確なら良いのですが、新入社員だと、何が分からないのかが分からない状態だったりもするようで。

オフライン・コミュニケーションだと隣にいる先輩の手が空いていそうなら、すいませんと声をかけるところでしょう。 その気安さがリモートワークだと作りづらい。

「Google Meet 繋ぎっぱなし」作戦は、リモートワークに少しだけオフライン・コミュニケーションの気楽さ・気安さを取り込めた良い試みだったと思っています。

この試みは、バーチャルオフィス・サービスが会社に導入されたことで、そちらで続けることになりました。

2026-05 退職へ

色々思うところがあって、2026 年 5 月 29 日付をもって株式会社 kubell を退職をしました。

写真は最終出社日に撮影したオフィスビルです。

転職先はまだ決まっていません。 iOS エンジニアとして働き続けたいと思っています。 もし私に興味のある企業さんがいらっしゃいましたら @at_aka に DM 頂ければ幸いです。

2026-03-10

オーディオ・オフ会 なお&トラさん邸訪問 (第 10 回)

10 回目のオーディオ・オフ会

2026-01-17 (土)、 なお&トラさん さん邸でのオーディオ・オフ会に参加しました。 前回、オーディオ・オフ会が 2015-04-04 (土) ですから、10 年以上ぶりの訪問となります。

Apple Music のプレイリスト

今回のオフ会で聴いた曲を見つけたものだけですが、Apple Music のプレイリストにまとめてみました。もし良ければ聴いてみてください。

経緯

お互い忙しかったのかもしれません。連絡を取る回数がいつしか減って、今、連絡しても大丈夫かしらんと変な遠慮が入り、気がつけば 10 年の月日が経っていました。

再会のきっかけは年賀状でした。 実は毎年、年賀状だけは出し続けていました。 2025 年の 1 月に「年賀状ありがとう」の返信を頂いて、また会いたいですね、というメッセージのやり取りをしました。 しかし、2025 年の 1 月は大きな風邪で寝込んでしまい、そのままお流れに。。。

そして、2026 年。 今年も去年末から 1 月にかけて大きな風邪を引き、実家で 1 週間近く寝込みました。 なんとか治って、実家から東京に戻ろうとしたところに、なお&トラさんから電話が一本。 あっという間にオーディオ・オフ会の日取りが決まりました。

サブシステム

最初にサブシステムで LP を 9 枚、聴かせてもらいました。 サブシステムはパラダイムのブックシェルフ・スピーカー Persona B を主軸にした構成です。

なお&トラさんと言えば、絵のような、写真のようなオーディオ表現を得意とされています。 裏を返せば、楽器の響きをメインに出さない音作りを好まれる印象でした。

サブシステムは過去の方向性を踏襲していて、スピーカーとスピーカーの間に音場が作られ、その空間で全ての音が明瞭に鳴っていました。 意外に思ったのは、ピアノの音がとても良く響くこと。 これは過去の方向性に対して大きな変化だと思いました。

ピアノの響きは中域がやや厚く、フワッと音が広がる空気感が出ています。 ピアノという楽器が弦から音を取り出し、響板をふるわせ、フレームを鳴らして響きが生まれる。 そんな様子を目の当たりにするような感覚です。 加えて、ピアノの低音は沈み込むよう。 すごく深く音が落ちていくように感じました。

少し気になったのは、左右のバランス。 右の響きが左に比べて、やや強いように思いました。

聴かせて頂いた LP から、いくつかピックアップして感想を書いてみます。

まずは、アシュケナージが弾くベートーヴェンのピアノソナタ第 23 番「熱情」。 アシュケナージはどちらかと言うとベートーヴェンを淡泊に弾く、という私の印象? 思い込み? が覆りました。 音の一つ一つが明確な意志を持って、なんと丁寧に扱われていることか! ドイツ的な音楽づくりではないのですが、ベートーヴェンの音楽づくりを真摯に取り組んでいるのだと知りました。 Decca 録音というのもあるかもしれませんが、鍵盤のタッチと底つきまで見えるようでした。

Yuko Mabuchi Trio のアルバム「Yuko Mabuchi Trio」から What is This Thing Called Love?。 ジャズのアルバムです。 ハイハットの空気感は弱め。 ドラムのアタック感がいたって自然。 そしてピアノが良い。 バランスとしてピアノを主役に立てるシステムだと感じました。

カティア・ブニアティシヴィリのアルバム「ラビリンス」は、私も愛聴の一枚。 映画「ワンス・アポン・ア・タイム・イン・アメリカ」からエンニオ・モリコーネ作曲の「デボラのテーマ」。 とても深い低音が味わえる一品。たまりません。

同アルバムから、フランソワ・クープランの「神秘的な障壁」も聴きました。 クープランはバロック時代の作曲家。 ピアノのなかった時代。 クラヴサンという楽器のために作曲された小品です。 CD を探すとチェンバロやクラヴサンで演奏した録音が見つかります。 ブニアティシヴィリはこの曲をピアノで演奏しました。 舟に揺られて行っては戻り行っては戻りを繰り返すような音楽が、ピアノでロマンティックに演奏されていて「ラビリンス」の中でもお気に入りの一曲です。 自分のオーディオ・システムで聴いていた時は気がつかなかったのですが、ブニアティシヴィリは強弱を大きくつけているのですね。 ニュアンスが掴めると、より音楽が楽しくなり、更にこの演奏が好きになりました。

ラモーのクラヴサン曲集から La Dauphine。 演奏は Albert Fuller。 チェンバロの演奏です。 チェンバロというと高音が耳にきつく低音が出ない、と思っていたのですが、全ての音が明瞭に聞こえるのにキツく感じなくて、低音もしっかりと鳴っていてとても楽しく音楽を味わいました。

メインシステム

メインシステムでは、私が持参した CD を 4 枚、なお&トラさんおすすめの CD を 5 枚、聴かせてもらいました。 メインシステムで使っているスピーカーは三菱のダイヤトーン 2S-3003 に改造を加えたもの。

サブシステムと比べると圧倒的に音場が広い! 目の前に音楽が広がります。 サブシステムで顕著だったピアノの「楽器としての響き」は抑えられ、音楽の描写力がより精緻です。 音楽の描写をメインに据えるオーディオ・マニアとしての芯は変わらないのだなぁと思いつつも、10 年前の記憶と比べてみると、少し響きが加わり、写真のような少し人工的な描写から自然な描写へと進化したように感じました。

ピアノの響きだけを取るならサブシステムの方が好きなのですが、全体的にはメインシステムの方が好み。 なぜ? と問われると答えづらいのですが、音楽を聴いていて楽しい。 おそらく音場の広さ・描写力の高さと響きのバランスの取り方が私の好みに合っているのではないかと思います。

メインシステムで聴いた CD も、感想を少しだけ書いておきます。

ベートーヴェン作曲「コリオラン序曲」、ブルーノ・ワルター指揮コロンビア交響楽団。 オーディオ視聴における私のリファレンス CD です。 管弦楽曲ということもありますが、音場の広さに圧倒されます。 スピーカーの少し先くらいまでしか音が広がらなかったサブシステムと比べて、こちらは部屋の端から端までステージが広がりました。 ホールトーンの響きも素晴らしい。

ベートーヴェンのピアノソナタ第 23 番「熱情」をギーゼキングの EMI 録音から。 ギーゼキングは私が大好きなピアニストです。 たまたま EMI のベートーヴェン、ピアノソナタ選集を持ってきていたので、アシュケナージに対抗してギーゼキングの「熱情」を聴かせてもらいました。 ギーゼキングはピアニッシモから大音量のフォルテッシモまで、ダイナミックレンジの広い演奏をしていたそうですが、EMI の「熱情」はその広いダイナミックレンジを捉えることに成功していません。 音量が大きくなったところで無惨なほどに音が割れてしまいます。 私のシステムではそこまで気にならなかったのですが、写実的に描くオーディオシステムでは録音の粗が逆に目立ってしまいました。 割れる音の中から、ギーゼキングはこんな音を鳴らしたかったんだろうなぁ、という意志は良く伝わってきて、録音が残念なことに涙する体験となりました。 無念です。

マイケル・ジャクソンの「ヒール・ザ・ワールド」。 曲の冒頭。 どこかの公園で録音したのでしょうか? 子供たちが後ろで遊び回っている様子が伺え、少女の語りが入ります。 音がね。輝いているようです。 感動して涙が出そうになりました。 マイケルの音作りの妥協のなさを堪能しました。

Solas のライブアルバム「Reunion」から Ni Na La。 Ni Na La は Solas のファーストアルバム「Solas」の 1 曲目に収録されている曲です。 アイリッシュ・トラッド曲なのかな? ライブアルバム「Reunion」は、その Solas の 10 周年記念ライブを録音したもの。 10 年の活動の中でグループから離れたメンバーもいて、そういう人達がこのライブで再び姿を見せているそうです。 録音はファーストアルバムの方が良いですが、ライブ盤には会場の熱気が記録されていて聴いているとこちらまで心がウキウキしてきます。 家のシステムではライブ会場の音が少し団子状になって聴こえます。 しかし、こちらのメインシステムでは音と音がしっかり分離されていて、ライブ会場にいるような感覚になりました。 ただ、ギーゼキングの「熱情」でも書いたように録音の粗を出してしまうのか、音が少しキツく感じることがありました。

ラストはイーグルスのライブアルバム「ヘル・フリーゼス・オーバー」からホテル・カリフォルニアを聴かせてもらいました。 目の前に観衆がいて、奥のステージに演者であるイーグルスがいる。 そして、観衆は観衆向けのマイクで録音されていて、イーグルスの演奏は全く別のマイクで録音している。 そういう音作りが目に見えて驚きました。 そんな気づきもイントロが終わるころには消えてしまい、音楽に合わせて体を揺らしていました。

あとがき

今回、ブログ環境を作り直したり、Apple Music のプレイリストを作ってみたりと、ブログの記事を書き始めるまでに随分時間がかかりました。 そして、ブログの記事を書き始めてからも、筆が進まず書き終えるまでにまた 3 週間近くかけてしまいました。 なんとか形になってホッとしています。

10 年ぶりということで自分のオーディオ勘が鈍っているのではないかと心配しましたが、いざオーディオ機器を前にすると 10 年前の印象を思い出すことができて驚きました。 これはオーディオが好きということも一因だと思いますが、インプットしたものを「ブログという形でアウトプットする」ことで記憶が定着したのではないかな? なんて思っています。 前回の記事でも月一くらいでブログの更新をしたいと書きましたが、続けられればと思います。

ref

2026-02-16

ブログ記事を Markdown で作成する

今までこのブログは HTML を直接書いていました。しかし、最近は Markdown でテキストを書くことが増え、HTML を直書きすることが億劫に感じます。そこで、Markdown から HTML に変換する仕組みに変えてみました。

あわせて、本ブログの文体も「ですます調」に切り替えてみます。

Pandoc の導入

Markdown から HTML ファイルを生成する仕組みとして Pandoc を使います。

Pandoc は各種文書フォーマットを相互変換するツールです。開発言語は Haskell。2006 年から開発が始まっており、2026-02-16 現在の最新バージョンは 3.9 です。

Pandoc のインストール

Pandoc は brew コマンドでインストールできます。

brew install pandoc
Pandoc の基本的な使い方

Pandoc の基本オプションは --from--to です。--from オプションで入力フォーマット指定します。今回は markdown を選択します。--to オプションは出力フォーマット指定です。出力フォーマットは html5 を選びます。

出力先は --output オプションで指定できます。

利用例は次の通りです:

pandoc --from=markdown --to=html5 --output=entry.html entry.md

上記コマンドは、Markdown ファイル entry.md を HTML に変換し、entry.html ファイルを作成します。

例えば entry.md が次のような中身だった場合

# Pandoc の導入

Markdown から HTML ファイルを生成する仕組みとして [Pandoc](https://pandoc.org/) を使います。

Pandoc は各種文書フォーマットを相互変換するツールです。...

## Pandoc のインストール

Pandoc は `brew` コマンドでインストールできます。

出力の entry.html は下記のようになります。

<h1 id="pandoc-の導入">Pandoc の導入</h1>
<p>Markdown から HTML ファイルを生成する仕組みとして <a
href="https://pandoc.org/">Pandoc</a> を使います。</p>
<p>Pandoc は各種文書フォーマットを相互変換するツールです。…</p>
<h2 id="pandoc-のインストール">Pandoc のインストール</h2>
<p>Pandoc は <code>brew</code> コマンドでインストールできます。</p>
markdown フォーマット

--from オプションに設定する markdown について補足です。 Pandoc には Markdown を表すフォーマットが 8 つあります:

  1. commonmark : CommonMark
  2. commanmark_x : 拡張付き CommonMark
  3. gfm : GitHub-Flavored Markdown
  4. markdown_github : deprecated になった gfm
  5. markdown : Pandoc’s Markdown
  6. markdown_mmd : MultiMarkdown
  7. markdown_phpextra : PHP Markdown Extra
  8. markdown_strict : オリジナルの (拡張なしの) Markdown

これらは Markdown の様々な方言に対応したものです。8 つのフォーマットのうち、どのフォーマットを使うのが適切でしょうか?

最もオリジナルに近い Markdown を使いたい場合は markdown_strict を使います。

GitHub-Flavored Markdown で導入された Fenced Code Block を使いたい場合は gfm です。と言いたいところですが、gfm には罠があります。実は gfm を指定すると、厳密に GitHub-Flavored Markdown な記法に限定されてしまうのです。言い換えると、gfm は Pandoc 用の拡張を受け付けません。

私は markdown フォーマットを選んでいます。markdown フォーマットは Pandoc 用の拡張をサポートした Markdown 用フォーマットです。HTML に変換する時に、例えば classid を付与することが可能になります。先の Fenced Code Block も markdown フォーマットはデフォルトで対応しています。

ブログ出力用のカスタマイズ

自動 ID 付与の抑制

Pandoc はヘッダーに自動で ID を振ってくれます。とても便利な機能ですが、ブログでは少し問題があります。というのは、ブログは複数の記事を 1 つのページに表示することがあるからです。

例えば、ブログの記事毎に「あとがき」なるヘッダーを入れたとします。この時 Pandoc は id="あとがき" という ID を自動生成します。読者がその記事だけを読むのであれば良いでしょう。しかし、「月別アーカイブ」を開くとどうでしょう。「月別アーカイブ」の中にいくつもの id="あとがき" が現れてしまいます。ID が複数あると、<a href="#あとがき">あとがきへのリンク</a> はどこへジャンプすれば良いのか分からなくなってしまいます。

Pandoc の自動 ID 付与機能をオフにするには、--format オプションに追加オプション -auto_identifiers を付けます。

pandoc --from=markdown-auto_identifiers --to=html5 entry.md > entry.html

以下、実行結果です:

<h1>Pandoc の導入</h1>
<p>Markdown から HTML ファイルを生成する仕組みとして <a
href="https://pandoc.org/">Pandoc</a> を使います。</p>
<p>Pandoc は各種文書フォーマットを相互変換するツールです。…</p>
<h2>Pandoc のインストール</h2>
<p>Pandoc は <code>brew</code> コマンドでインストールできます。</p>
ID の指定

ブログ記事を書いていると、ID を付けたいケースもあります。その場合、Pandoc 拡張を利用します。

# Pandoc の導入{#introduce-pandoc}

上記のように、# ヘッダー文字列{#id-text} という形で ID を記入します。すると、次のように ID が振られます。

<h1 id="introduce-pandoc">Pandoc の導入</h1>
ヘディング・レベルの変更

Blogger では HTML のヘディング・レベルは次のようになっています。

  • h1 : ブログのタイトル
  • h2 : 日付ヘッダー
  • h3 : 記事タイトル

従って、ブログ記事内の最初のヘッダーは h4 から始まります。ですので、Markdown でブログ記事を書く時も h4 が始まりとなります。

ですが、いちいち書き手がそんな気を回さなければいけないのは面倒です。幸い Pandoc には Shift Heading Level という機能があり、ヘディング・レベルを変更できます。オプションは --shift-heading-level-by を使います。今回は # (h1 相当) を #### (h4 相当) にシフトさせたいので、ヘディング・レベルを 3 つ上げます。

pandoc --from=markdown-auto_identifiers --to=html5 --shift-heading-level-by=3 entry.md > entry.html

これで次のように #h4 に変換されます。

<h4>Pandoc の導入</h4>
<p>Markdown から HTML ファイルを生成する仕組みとして <a
href="https://pandoc.org/">Pandoc</a> を使います。</p>
<p>Pandoc は各種文書フォーマットを相互変換するツールです。…</p>
<h5>Pandoc のインストール</h5>
<p>Pandoc は <code>brew</code> コマンドでインストールできます。</p>

コードの表示

Pandoc のシンタックス・ハイライト

Pandoc はソースコードのシンタックス・ハイライトをデフォルトでサポートしています。

例えば、次のソースコードを含む Markdown ファイルを HTML に変換してみましょう。

Lua のサンプルコードです。

``` lua
local languages = {
  ['emacs-lisp'] = true,
  elisp = true,
  html = true,
  lua = true,
  markdown = true,
  ruby = true,
  swift = true,
}
```

上記の Markdown ファイルに対して、次のコマンドを実行してみました。

pandoc --from=markdown-auto_identifiers --to=html5 --shift-heading-level-by=3 --output=source1.html source1.md

結果は次のようになります。

<p>Lua のサンプルコードです。</p>
<div class="sourceCode" id="cb2"><pre
class="sourceCode lua"><code class="sourceCode lua"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="kw">local</span> <span class="va">languages</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>  <span class="op">[</span><span class="st">&#39;emacs-lisp&#39;</span><span class="op">]</span> <span class="op">=</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>  <span class="va">elisp</span> <span class="op">=</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a>  <span class="va">html</span> <span class="op">=</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a>  <span class="va">lua</span> <span class="op">=</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a>  <span class="va">markdown</span> <span class="op">=</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a>  <span class="va">ruby</span> <span class="op">=</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a>  <span class="va">swift</span> <span class="op">=</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>

どうでしょう。ソースコードの構文が解析され、変数やオペレーターに span 要素が追加されています。ただ、CSS が当たっていないので色が付いていません。

ここで --standalone オプションを追加すると、HTML ファイルに CSS を追加してくれます。

pandoc --from=markdown-auto_identifiers --to=html5 --shift-heading-level-by=3 --standalone --output=source1.html source1.md
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta charset="utf-8" />
  <meta name="generator" content="pandoc" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
  <title>source1</title>
  <style>
    /* Default styles provided by pandoc.
    ** See https://pandoc.org/MANUAL.html#variables-for-html for config info.
    */
    html {
      color: #1a1a1a;
      background-color: #fdfdfd;
    }
    body {
      margin: 0 auto;
      max-width: 36em;
      padding-left: 50px;
      padding-right: 50px;
      padding-top: 50px;
      padding-bottom: 50px;
      hyphens: auto;
      overflow-wrap: break-word;
      text-rendering: optimizeLegibility;
      font-kerning: normal;
    }
    @media (max-width: 600px) {
      body {
        font-size: 0.9em;
        padding: 12px;
      }
      h1 {
        font-size: 1.8em;
      }
    }
    @media print {
      html {
        background-color: white;
      }
      body {
        background-color: transparent;
        color: black;
        font-size: 12pt;
      }
      p, h2, h3 {
        orphans: 3;
        widows: 3;
      }
      h2, h3, h4 {
        page-break-after: avoid;
      }
    }
    p {
      margin: 1em 0;
    }
    a {
      color: #1a1a1a;
    }
    a:visited {
      color: #1a1a1a;
    }
    img {
      max-width: 100%;
    }
    svg {
      height: auto;
      max-width: 100%;
    }
    h1, h2, h3, h4, h5, h6 {
      margin-top: 1.4em;
    }
    h5, h6 {
      font-size: 1em;
      font-style: italic;
    }
    h6 {
      font-weight: normal;
    }
    ol, ul {
      padding-left: 1.7em;
      margin-top: 1em;
    }
    li > ol, li > ul {
      margin-top: 0;
    }
    blockquote {
      margin: 1em 0 1em 1.7em;
      padding-left: 1em;
      border-left: 2px solid #e6e6e6;
      color: #606060;
    }
    code {
      font-family: Menlo, Monaco, Consolas, 'Lucida Console', monospace;
      font-size: 85%;
      margin: 0;
      hyphens: manual;
    }
    pre {
      margin: 1em 0;
      overflow: auto;
    }
    pre code {
      padding: 0;
      overflow: visible;
      overflow-wrap: normal;
    }
    .sourceCode {
     background-color: transparent;
     overflow: visible;
    }
    hr {
      border: none;
      border-top: 1px solid #1a1a1a;
      height: 1px;
      margin: 1em 0;
    }
    table {
      margin: 1em 0;
      border-collapse: collapse;
      width: 100%;
      overflow-x: auto;
      display: block;
      font-variant-numeric: lining-nums tabular-nums;
    }
    table caption {
      margin-bottom: 0.75em;
    }
    tbody {
      margin-top: 0.5em;
      border-top: 1px solid #1a1a1a;
      border-bottom: 1px solid #1a1a1a;
    }
    th {
      border-top: 1px solid #1a1a1a;
      padding: 0.25em 0.5em 0.25em 0.5em;
    }
    td {
      padding: 0.125em 0.5em 0.25em 0.5em;
    }
    header {
      margin-bottom: 4em;
      text-align: center;
    }
    #TOC li {
      list-style: none;
    }
    #TOC ul {
      padding-left: 1.3em;
    }
    #TOC > ul {
      padding-left: 0;
    }
    #TOC a:not(:hover) {
      text-decoration: none;
    }
    code{white-space: pre-wrap;}
    span.smallcaps{font-variant: small-caps;}
    div.columns{display: flex; gap: min(4vw, 1.5em);}
    div.column{flex: auto; overflow-x: auto;}
    div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
    /* The extra [class] is a hack that increases specificity enough to
       override a similar rule in reveal.js */
    ul.task-list[class]{list-style: none;}
    ul.task-list li input[type="checkbox"] {
      font-size: inherit;
      width: 0.8em;
      margin: 0 0.8em 0.2em -1.6em;
      vertical-align: middle;
    }
    .display.math{display: block; text-align: center; margin: 0.5rem auto;}
    /* CSS for syntax highlighting */
    html { -webkit-text-size-adjust: 100%; }
    pre > code.sourceCode { white-space: pre; position: relative; }
    pre > code.sourceCode > span { display: inline-block; line-height: 1.25; }
    pre > code.sourceCode > span:empty { height: 1.2em; }
    .sourceCode { overflow: visible; }
    code.sourceCode > span { color: inherit; text-decoration: inherit; }
    div.sourceCode { margin: 1em 0; }
    pre.sourceCode { margin: 0; }
    @media screen {
    div.sourceCode { overflow: auto; }
    }
    @media print {
    pre > code.sourceCode { white-space: pre-wrap; }
    pre > code.sourceCode > span { text-indent: -5em; padding-left: 5em; }
    }
    pre.numberSource code
      { counter-reset: source-line 0; }
    pre.numberSource code > span
      { position: relative; left: -4em; counter-increment: source-line; }
    pre.numberSource code > span > a:first-child::before
      { content: counter(source-line);
        position: relative; left: -1em; text-align: right; vertical-align: baseline;
        border: none; display: inline-block;
        -webkit-touch-callout: none; -webkit-user-select: none;
        -khtml-user-select: none; -moz-user-select: none;
        -ms-user-select: none; user-select: none;
        padding: 0 4px; width: 4em;
        color: #aaaaaa;
      }
    pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa;  padding-left: 4px; }
    div.sourceCode
      {   }
    @media screen {
    pre > code.sourceCode > span > a:first-child::before { text-decoration: underline; }
    }
    code span.al { color: #ff0000; font-weight: bold; } /* Alert */
    code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
    code span.at { color: #7d9029; } /* Attribute */
    code span.bn { color: #40a070; } /* BaseN */
    code span.bu { color: #008000; } /* BuiltIn */
    code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
    code span.ch { color: #4070a0; } /* Char */
    code span.cn { color: #880000; } /* Constant */
    code span.co { color: #60a0b0; font-style: italic; } /* Comment */
    code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
    code span.do { color: #ba2121; font-style: italic; } /* Documentation */
    code span.dt { color: #902000; } /* DataType */
    code span.dv { color: #40a070; } /* DecVal */
    code span.er { color: #ff0000; font-weight: bold; } /* Error */
    code span.ex { } /* Extension */
    code span.fl { color: #40a070; } /* Float */
    code span.fu { color: #06287e; } /* Function */
    code span.im { color: #008000; font-weight: bold; } /* Import */
    code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
    code span.kw { color: #007020; font-weight: bold; } /* Keyword */
    code span.op { color: #666666; } /* Operator */
    code span.ot { color: #007020; } /* Other */
    code span.pp { color: #bc7a00; } /* Preprocessor */
    code span.sc { color: #4070a0; } /* SpecialChar */
    code span.ss { color: #bb6688; } /* SpecialString */
    code span.st { color: #4070a0; } /* String */
    code span.va { color: #19177c; } /* Variable */
    code span.vs { color: #4070a0; } /* VerbatimString */
    code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
  </style>
</head>
<body>
<p>Lua のサンプルコードです。</p>
<div class="sourceCode" id="cb2"><pre
class="sourceCode lua"><code class="sourceCode lua"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="kw">local</span> <span class="va">languages</span> <span class="op">=</span> <span class="op">{</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>  <span class="op">[</span><span class="st">&#39;emacs-lisp&#39;</span><span class="op">]</span> <span class="op">=</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a>  <span class="va">elisp</span> <span class="op">=</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a>  <span class="va">html</span> <span class="op">=</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a>  <span class="va">lua</span> <span class="op">=</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a>  <span class="va">markdown</span> <span class="op">=</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a>  <span class="va">ruby</span> <span class="op">=</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a>  <span class="va">swift</span> <span class="op">=</span> <span class="kw">true</span><span class="op">,</span></span>
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div>
</body>
</html>

ウェブブラウザーで HTML ファイルを開くと、色が付いているのが分かります。

Prism.js を使ったシンタックス・ハイライト

Pandoc のシンタックス・ハイライトを使っても良いですが、せっかく前記事で Prism.js を導入したので、シンタックス・ハイライトはそちらに任せようと思います。

Prism.js を使う場合、Pandoc のデフォルトで付いてくるシンタック・ハイライトの出力が邪魔です。まず --standalone オプションを 付けない ことで、CSS の挿入を妨げ、続いて --syntax-highlighting=none オプションを付けてシンタックス・ハイライト用の出力も抑制します。

pandoc --from=markdown-auto_identifiers --to=html5 --shift-heading-level-by=3 --syntax-highlighting=none --output=source1.html source1.md

すると、シンプルな HTML 出力を得ました。

<p>Lua のサンプルコードです。</p>
<pre class="lua"><code>local languages = {
  [&#39;emacs-lisp&#39;] = true,
  elisp = true,
  html = true,
  lua = true,
  markdown = true,
  ruby = true,
  swift = true,
}</code></pre>

さて、Prism.js にシンタックス・ハイライトをさせるためには、次の書式で HTML を書く必要がありました。 これは HTML5 で推奨されている書き方だそうです。

<pre><code class="language-XXX">
プログラミング言語 XXX のソースコード
</code></pre>

HTML5 の推奨は code 要素の class 属性に language-lua と書くのですが、Pandoc の出力は pre 要素の class 属性に lua と書なっています。

Pandoc には Lua 言語でフィルターを作成することができて、Pandoc の HTML 出力を Prism.js に合わせることが可能です。

上記サイトを参考にして、Lua フィルタースクリプトを書きました。これを standard_code.lua という名前で保存します。 なお、保存する時に、シンタックスハイライトしたい言語を languages に記述してください。

--- standard-code: ouput code blocks with class="language-*" attributes
-- © 2020 Aman Verma. Distributed under the MIT license.

-- Check keywords at https://prismjs.com/#supported-languages
local languages = {
  ['emacs-lisp'] = true,
  elisp = true,
  html = true,
  lua = true,
  makefile = true,
  markdown = true,
  ruby = true,
  shellsession = true,
  swift = true,
}

-- local languages = {meta = true,markup = true,css = true,clike = true,javascript = true,abap = true,abnf = true,actionscript = true,ada = true,agda = true,al = true,antlr4 = true,apacheconf = true,apl = true,applescript = true,aql = true,arduino = true,arff = true,asciidoc = true,aspnet = true,asm6502 = true,autohotkey = true,autoit = true,bash = true,basic = true,batch = true,bbcode = true,bison = true,bnf = true,brainfuck = true,brightscript = true,bro = true,bsl = true,c = true,csharp = true,cpp = true,cil = true,clojure = true,cmake = true,coffeescript = true,concurnas = true,csp = true,crystal = true,['css-extras'] = true,cypher = true,d = true,dart = true,dax = true,dhall = true,diff = true,django = true,['dns-zone-file'] = true,docker = true,ebnf = true,editorconfig = true,eiffel = true,ejs = true,['emacs-lisp'] = true,elisp = true,elixir = true,elm = true,etlua = true,erb = true,erlang = true,['excel-formula'] = true,fsharp = true,factor = true,['firestore-security-rules'] = true,flow = true,fortran = true,ftl = true,gml = true,gcode = true,gdscript = true,gedcom = true,gherkin = true,git = true,glsl = true,go = true,graphql = true,groovy = true,haml = true,html = true,handlebars = true,haskell = true,haxe = true,hcl = true,hlsl = true,http = true,hpkp = true,hsts = true,ichigojam = true,icon = true,ignore = true,inform7 = true,ini = true,io = true,j = true,java = true,javadoc = true,javadoclike = true,javastacktrace = true,jolie = true,jq = true,jsdoc = true,['js-extras'] = true,json = true,json5 = true,jsonp = true,jsstacktrace = true,['js-templates'] = true,julia = true,keyman = true,kotlin = true,latex = true,latte = true,less = true,lilypond = true,liquid = true,lisp = true,livescript = true,llvm = true,lolcode = true,lua = true,makefile = true,markdown = true,['markup-templating'] = true,matlab = true,mel = true,mizar = true,mongodb = true,monkey = true,moonscript = true,n1ql = true,n4js = true,['nand2tetris-hdl'] = true,naniscript = true,nasm = true,neon = true,nginx = true,nim = true,nix = true,nsis = true,objc = true,objectivec = true,ocaml = true,opencl = true,oz = true,parigp = true,parser = true,pascal = true,pascaligo = true,pcaxis = true,peoplecode = true,perl = true,php = true,phpdoc = true,['php-extras'] = true,plsql = true,powerquery = true,powershell = true,processing = true,prolog = true,properties = true,protobuf = true,pug = true,puppet = true,pure = true,purebasic = true,purescript = true,python = true,q = true,qml = true,qore = true,r = true,racket = true,jsx = true,tsx = true,reason = true,regex = true,renpy = true,rest = true,rip = true,roboconf = true,robotframework = true,ruby = true,rust = true,sas = true,sass = true,scss = true,scala = true,scheme = true,['shell-session'] = true,smali = true,smalltalk = true,smarty = true,solidity = true,['solution-file'] = true,soy = true,sparql = true,['splunk-spl'] = true,sqf = true,sql = true,stan = true,iecst = true,stylus = true,swift = true,['t4-templating'] = true,['t4-cs'] = true,['t4-vb'] = true,tap = true,tcl = true,tt2 = true,textile = true,toml = true,turtle = true,twig = true,typescript = true,typoscript = true,unrealscript = true,vala = true,vbnet = true,velocity = true,verilog = true,vhdl = true,vim = true,['visual-basic'] = true,warpscript = true,wasm = true,wiki = true,xeora = true,['xml-doc'] = true,xojo = true,xquery = true,yaml = true,yang = true,zig = true}


local function escape(s)
  -- Escape according to HTML 5 rules
  return s:gsub(
    [=[[<>&"']]=],
    function(x)
      if x == '<' then
        return '&lt;'
      elseif x == '>' then
        return '&gt;'
      elseif x == '&' then
        return '&amp;'
      elseif x == '"' then
        return '&quot;'
      elseif x == "'" then
        return '&#39;'
      else
        return x
      end
    end
  )
end

local function getCodeClass(classes)
  -- Check if the first element of classes (pandoc.CodeBlock.classes) matches a
  -- programming language name. If it does, it gets removed from classes and a valid
  -- HTML class attribute string (with space at beginning) is returned.

  if languages[classes[1]] then
    return ' class="language-' .. table.remove(classes, 1) .. '"'
  else
    return ''
  end
end

local function makeIdentifier(ident)
  -- Returns a valid HTML id attribute (with space at beginning) OR empty string.

  if #ident ~= 0 then
    return ' id="'.. ident .. '"'
  else
    return ''
  end
end

local function makeClasses(classes)
  -- Returns a valid HTML class attribute with classes separated by spaces (with a space
  -- at the beginning) OR empty string.

  if #classes ~= 0 then
    return ' class="' .. table.concat(classes, ' ') .. '"'
  else
    return ''
  end
end

local function makeAttributes(attrs)
  -- Returns a string of HTML attributes from key-value pairs
  -- (with space at beginning for each attribute) OR empty string.

  if #attrs ~= 0 then
    local result = ''
    for key, value in pairs(attrs) do
      result = result .. ' ' .. key .. '="' .. escape(value) .. '"'
    end
    return result
  else
    return ''
  end
end

return {
  {
    CodeBlock = function(elem)
      if FORMAT ~= 'html5' then
        return nil
      end

      local id = makeIdentifier(elem.identifier)
      local classLang = getCodeClass(elem.classes)
      local classReg = makeClasses(elem.classes)
      local extraAttrs = makeAttributes(elem.attributes)

      local preCode = string.format(
        '<pre%s%s%s><code%s>%s</code></pre>', id, classReg, extraAttrs, classLang, escape(elem.text)
      )
      return pandoc.RawBlock('html', preCode, 'RawBlock')
    end,

  }
}

--lua-filter=standard_code.lua オプションで Lua スプリプトを指定します。

pandoc --from=markdown-auto_identifiers --to=html5 --shift-heading-level-by=3 --syntax-highlighting=none --lua-filter=standard_code.lua --output=source1.html source1.md

出力を見てみましょう。Prism.js 向けの HTML になりました。

<p>Lua のサンプルコードです。</p>
<pre><code class="language-lua">local languages = {
  [&#39;emacs-lisp&#39;] = true,
  elisp = true,
  html = true,
  lua = true,
  markdown = true,
  ruby = true,
  swift = true,
}</code></pre>
行番号を表示する

Prism.js にはプラグインを導入することで、行番号を表示することができます。 そのためには、class="line-numbers" を指定する必要があります。 Pandoc では {.line-numbers} と書くことでクラスを追加できます。

実際に試してみます。 Markdown ファイルに {.line-numbers} を追加した後、

Lua のサンプルコードです。

``` lua {.line-numbers}
local languages = {
  ['emacs-lisp'] = true,
  elisp = true,
  html = true,
  lua = true,
  markdown = true,
  ruby = true,
  swift = true,
}
```

pandoc コマンドを実行します。

pandoc --from=markdown-auto_identifiers --to=html5 --shift-heading-level-by=3 --syntax-highlighting=none --lua-filter=standard_code.lua --output=source2.html source2.md

出力は次の通り。 行番号対応した HTML が得られました。

<p>Lua のサンプルコードです。</p>
<pre class="line-numbers"><code class="language-lua">local languages = {
  [&#39;emacs-lisp&#39;] = true,
  elisp = true,
  html = true,
  lua = true,
  markdown = true,
  ruby = true,
  swift = true,
}</code></pre>
行をハイライトする

行をハイライトする方法も見ておきましょう。 ハイライトしたい行を data-line="行番号" という形で指定するのでした。 Pandoc では {data-line="5"} と指定すれば OK です。

こちらも試してみましょう。 Markdown ファイルに data-line="5" を追加してみます。

Lua のサンプルコードです。

``` lua {.line-numbers data-line="5"}
local languages = {
  ['emacs-lisp'] = true,
  elisp = true,
  html = true,
  lua = true,
  markdown = true,
  ruby = true,
  swift = true,
}
```

これを pandoc で処理します。

pandoc --from=markdown-auto_identifiers --to=html5 --shift-heading-level-by=3 --syntax-highlighting=none --lua-filter=standard_code.lua --output=source3.html source3.md

行ハイライトに対応した HTML が出力されていますね。

<p>Lua のサンプルコードです。</p>
<pre class="line-numbers" data-line="5"><code class="language-lua">local languages = {
  [&#39;emacs-lisp&#39;] = true,
  elisp = true,
  html = true,
  lua = true,
  markdown = true,
  ruby = true,
  swift = true,
}</code></pre>

Makefile の作成

以上の準備を整えれば、Markdown ファイルを pandoc コマンドで HTML ファイルに変換ができます。 しかし、必要な pandoc コマンドはオプションを含めると長くなってしまいました。 これだけのオプションを毎回手打ちするのは避けたいところです。

そこで、Makefile を作って自動化することにしました。

Markdown ファイルと同じディレクトリーに、次のコードを Makefile という名前で保存します。

all: html

PANDOC = pandoc
PANDOC_OPTS = --from=markdown-auto_identifiers --to=html5 --shift-heading-level-by=3 --syntax-highlighting=none --lua-filter=standard_code.lua

MD_FILES := $(wildcard *.md)
HTML_FILES := $(MD_FILES:.md=.html)

.PHONY: html
html: $(HTML_FILES)

%.html: %.md
    $(PANDOC) $(PANDOC_OPTS) --output=$@ $<

この Makefile.md という拡張子のファイルがあったら、pandoc コマンドを PANDOC_OPTS オプションを使って処理し、同名の .html ファイルを作成する、というものです。 Makefile を作っておけば、make と実行するだけで、HTML ファイルが作成されます。

make

あとがき

以上で設定は終わりです。 最近はめっきりブログを書くことが少なくなりました。 Markdown で気軽に書けるシステムを構築できたので、月一くらいで記事を書ければと思います。