MailDeliver2に「確認できる着信通知」

動機

Cinnamonの通知機能は使いにくい。

通知はずっと残すか(どんどん増えて超重くなる)、表示が終了したら消すかしかない。
メールの着信通知をNotify Sendで出しているのだけれども、見逃していまうと確認しようがないと、通知が常時きてるような環境ではその時に見られないので、まず確認できない。

着信に気づいてから確認したいのだが、その機能がなかった。
そこで、かねてからの希望だった、「Systrayに常駐し、クリックするとこれまで受け取ったメールが確認できる」という機能を追加することにした。

30分くらい、と思ったが5時間ほどかかった。

苦戦

まずSystrayをどう実現するかだが、yadを使った。
Zenityのforkらしいのだけれども、なんかだいぶ違う。

yadをどう使うかというのは随分悩んだ。
yadの--commandではクォートがエスケープされてしまうため、スペースで必ず分割されてしまう。そのため、置き換えが発生する部分は別にくくりだすしかない。
--listenで実行するコマンドを随時更新できるが、ここでも同様の問題があるし、そもそも標準入力から渡す方法というのは結構限られる。

結局3つに分割している。データ整形用のスクリプト、yadのcommandになるスクリプト、そしてyad自体を起動するスクリプトだ。

当初、4つだった。一時的に保存され、通知と共に消されてしまうヘッダをとっておくためのプラグインだが、不毛なので(そしてそれが必要になるプラグインはおそらく多いので)Maildeliver本体に組み込まれ、LeaveHeadersという設定を可能にした。

時間がかかったのは細かいミスが重なったわけで、未知のエラーには遭遇していない。

いずれにせよ、これでメールを遡って確認できるようになった。ダイアログのOKでクリア、cancelで保持だ。

Yadの起動をRubyで行っている理由は結構単純で、複雑なエスケープなしにスペースやspecial characterを含むエントリを分割するために行志向にしていて、行単位でargを与えるのがRubyだと楽だから。

Yadのspecial character

結局yadでなくZenityを使っていたのは、Yadはエントリーに対して結構複雑なエスケープを必要とするためで、とりあえず手っ取り早い方法としてである。

簡単にいえば、Yadの--listでエントリが表示されなかったり、ひとつ上の内容が表示されたりしていた。
ドキュメントにないので自力で試した限り

  • <または&が含まれる場合、そのエントリはテーブル的に上の項目をクローンする
  • エスケープ方法はXML(&lt;及び&amp;)
  • ところが&rt;には対応しておらず、&が含まれるためクローンされてしまう

こういうわけのわからない仕様はやめてほしい。

文系プログラマはそんなところで挫折しない

原文に言及

Paizaのプログラミングの勉強を始めたときに、文系が挫折しやすい7つのポイントという記事に猛烈な違和感があり、ついでなので巷に溢れる理系だ文系だ文系プログラマだとかいう風説を否定していこうと思う。

既にTwitterで言及されたりしているのだが、明らかに「プログラミングの勉強を始めたときに、文系が挫折しやすい7つのポイント」ではなく、「プログラミングの勉強を始めたときに、文系の私が挫折した7つのポイント」だ。

予め言っておこう。私は文系である。脳はバリバリの理系思考だが、興味分野は文系であり、教科としての得意分野も文系科目なので、世の中的には文系ということになる。だが、私は2歳の時にはプログラミングをやっていたし、幼少期は際立って天才的だったのだが、それでも言語発達よりもプログラミング習得のほうが早かった。ひらがなをマスターしたのは4歳になってからなので、ひらがなよりもアルファベットが先にあった。

Hello, World!

別にそのようなことは微塵も思わないし、それは文系か理系かということとは全く別次元のところにある。

簡単にいえば、「テレビが大好きな人はプログラミングに向かない」のだ。

テレビを視聴している時、脳は停止している、ということが既に研究により判明している。だが、おそらくこれは真実ではない。私はテレビを観ていようが、非常に活発に様々なことを考えている。そして、その速度に追いつかないことや内容が稚拙なこと、私の疑問に応えないことにいつも苛立っている。だから、私はTVが大嫌いだ。

対して、ゲームが好きな人は向いている。ドライブなら助手席が好きな人より運転席が好きな人が向いているし、絵を見るのが好きな人よりも絵を描く人が向いているし、ラジコンが好きな人はかなり向いている。

要は自分が支配し、操縦することが好きな人は向いていて、受け身な人は向いていない。

さらにいえば、好奇心の多寡が重大な違いにもなる。

この節において、この人は単に受動的すぎて、しかも好奇心によって駆動してもいない、テレビが好きなタイプであるために受け入れられなかった、というだけだろう。

だいたい、そもそもこれを「暗記する」と考える時点で向いていない。「どうなっているんだろう」と思ったあなたは向いている。

#include <stdio.h>

int main(void)
  {
      printf("Hello, world!");
      return 0;
  }

おまじない

これは、よく使われるが、よく知られた悪しきものである。
単にきちんと説明する力がないのだろう。理解させる必要がないのかもしれないが。

C言語の#includeは、含める、そのまま「取り込み」に用いるものだ。stdioはSTanDard Input/Outputであり、読み込んだり、書き出したりする機能を持っている。
この機能を利用するために

#include <stdio.h>

と書いておく。

C言語の基本部分はただの骨組みだ。機能は別パーツになっている。これを取り付けるのがこの作業だ。

「何でも入る型ひとつあればいいのでは」は、疑問としては正しいし、意見としても構わない。汎用型を持っているものもあるし、Perlは静的型の言語だが、スカラー変数にはスカラー値であればなんでも入る。

これは思想的な問題である。C言語がそうなっているだけだ。
もちろん、メリットはある。気になるならそれを調べればいいし、そんな簡単に思いつくことは誰かが試している可能性が高いのだから、それを調べてもいい。

だが、躓きはしないだろう。学習を困難にさせる部分であり、なるべく型を意識させない言語というのは、私がプログラミングを教える上で重要な条件にはなっているが、別にそれは文系だからという問題でもない。

なお、なぜ型があるか?という質問に対する応えは明瞭だ。

トマトは冷蔵庫にいれなくてはいけないが、猫を冷蔵庫にいれるわけにはいかない。別な扱いが必要だと区別する必要があるだろう?

セミコロン

これも、行の切れ目が文の切れ目ということでいいじゃないか、はそのような思想の言語はたくさんある。

Perlは常にセミコロンで終端する。各種UNIXシェルは通常、行末を文ターミネータとして扱う。

RubyやJavaScriptは、「文脈的に次が続きであれば続き、そうでないければ終端とみなす」という扱いだ。

どれも一長一短だ。Perlの統一はわかりやすいが、同時に忘れやすい。

my $a = "Something";
print $a;

シェルの分割されるのは思わぬトラブルの元だ。

text="Hello,
This is Aki!"

これはエラーになる。でもZshではエラーにならない。

Rubyであればこういうこともできる。

array = [ "a", "b",
          "c", "d"]

JavaScriptも同じようにできるが、シェルで分割される文字列の改行は許されない。

よほど変なルールならともかく、C言語の場合はだいたい「セミコロンをつける」で統一されている。
それを「つまずく」と言っているのは、もはや言いがかりをつけたい気持ちであるだけなのではないか。

ループ

ループが面倒だというのはあって、そのあたりRubyが素晴らしい処理を行っている。JavaScriptライブラリもそれにならったものが多い。

「何を意味しているんだ」というのをつまずくと言ってしまうのならば、それは学習意欲の欠如を恥じるべきだと思うけれども。

3 part for loopは結構わかりにくいので、工夫が必要だ。

for (a = 0; a < 11; a++) {

11回ループなのだからそれほどわかりにくいこともないと思うが、別に1 originにしたいのであれば

for (a = 1; a <= 11; a++) {

とか至って構わないわけだ。もちろん、これはわかりづらいので、Rubyはこのループを排除している。

11.times do |a|

だが、この3 part forが欲しいケースは、全然違うところであったりする。まつもとさんが大嫌いなので、Rubyに入ることは絶対にないが。

for (<>; ! ~ /^END$/; <>) {

Perlになれている人以外はまるでわからないと思うが、これは、「各行を読み、それが「END」だけの行でないならばループを続ける」ということだ。

この応用として

for ( my @lines; ! ~ /^END$/; <>) { push(@lines, $_) }

というスマートなループが書けたりする。

まぁ、こんなものはお遊びだが、ループする必要性は簡単だ。日常的に、「食べ終わるまでごはんを食べるのとおかずを食べるのを繰り返す」といったループを、送っているはずなのだから。

配列

1クラス40人いたとして、出席簿を40冊用意してそれにタイトルを書きたいか?「出席簿」というタイトルで40人並べるほうが絶対楽だろう?

それだけのことだ。というか、配列なんていくら文系でも数学で習うだろう。

配列でつまずくとかわけがわからないよ。

ポインタ

確かに難易度は高いし、もっとよしなにやってくれと思うところではあるけれども、結局それが参照なのか実体なのかというのは常に問題になるのだ。

だが、これは文系がどうかには全く関係がない。文系だって「Aさんが佐藤だと思っている人物と、Bさんが鈴木だと思っている人物は、同一人物である」みたいな文章が出てくれば理解できるはずなのだから。

まとめに対して

本人も意欲のせいだということを言ってはいるが、そのあとがおかしい。

「なぜなのか?どうなっているのか?なんのために?どういう意味?」それを気にするのは理系の傾向であって文系の傾向ではないし(理系は証明する学問だ)、非常に重要な資質なのだ。

「そういうものだからで済ませる」というのは、明らかに向いていない人だ

それは、理系だの文系だのという話ではなく、理知に対する姿勢だと思う。どうでもいいことばかりを考えて難癖つけたがるというのは、もっと単純に「意欲がないし、取り組む姿勢もない」というだけの話で、文系に対する風評被害を生じさせるのはやめていただきたい。

実際文系プログラマってどうなの

プログラミングなんて9割方文系領域なので、別に何も問題ない。

クリエイティブであるはずの料理を単純作業にできるように、世の中ではプログラマの仕事が単純作業だったりもするわけで、そこはスキルなんてほぼ不要だったりもする。やる気なんかなくても覚えたことを繰り返すだけでいいという世界もある。

優れたスキルが欲しいのだとしても、別に文系だから不利ということは特にない。理知的であれば良いのだ。

むしろ、私が持っている「記憶力」というハンデのほうがはるかに大きい。

様々なブラウザと様々なプロファイルを切り替えて使うスクリプト

基本解説

GitHub

これまでFirefox Latestスクリプトを使ってきたが、これではFirefoxしか利用できなかったり、異常終了やフリーズすると問題が生じる可能性があった。

Firefox Latestの仕組みは次のようなものだ。

Gist

要はシンボリックリンクで.mozillaを切り替えて起動→終了したらシンボリックリンク修正、をしているだけだ。

ちなみに、もともとはMageiaがFirefoxをLTS版を採用していて、それが気に入らなかったのでこのようにしている。
だが、Manjaroはその必要はないので、Firefoxだけでなく、任意のブラウザを任意のプロファイルで起動できるようなスクリプトを書いた。

多くの関数が定義されているが、まずはプロファイル切り替えの方法をまとめる必要があった。

Chromium系は--user-data-dirオプションを使用する。

これに該当するのは

  • Google Chrome
  • Chromium
  • Opera (Blink系)
  • Vivaldi
  • SRWare Iron
  • Maxthon

Maxthon for LinuxはあまりにもOUT OF DATEが過ぎるが。

Firefox系は-P profile-profile pathのふたつ。設定全体を切り替えることはできず、プロファイルのみの保存切り替えだが、問題はあまりなさそうだった。これに該当するのは

  • Firefox
  • SeaMonkey
  • Palemoon

それ以外

  • Midoriは-c directory
  • qupzillaは-p directory
  • Rekonqは--config directory

この5タイプに対応した上で、それぞれのブラウザに対応した関数を用意し、簡単に起動できるようにしている。

例えば

mybrowsers[shopping]="chr $HOME/.browser-settings/chromium/shopping"

とした上で、

$ mybrowser.zsh shopping

とすれば、$HOME/.browser-settings/chromium/shoppingを設定ディレクトリとしてChromiumが起動することになる。

Zsh Assoc

今回のスクリプトではZshの連想配列を2つ使用している。

ひとつは、ブラウザの起動設定を行うためのものだ。これは

eval "$mybrowsers[$1]"

という部分で作用している。

もうひとつは、ブラウザのコマンドを修正するものだ。Chromeはgoogle-chromeだったりするかと思うが、Archではgoogle-chrome-stableだったりもする。

そこでこれを修正するため、ブラウザの各コマンドは"${${modify_browsers[$browser]}:-$browser}"とすることで修正可能にした。

PureBuilder2 TopicPath機能の追加

変更点

最新の変更@Gist

概要

これまでTopicPath機能はPureBuilderで提供されて来なかった。
そのため、個別のドキュメントとテンプレートにおいて実装可能な機能として紹介されてきたが、一般的な要望であるため今回の変更で取り込んだ。

このTopicPath機能は整形されたHTMLを返すわけではなく、シンボルと文字列からなる配列を返す。
PureBuilderの本来の設計に従い、サイトの階層構造に等しいディレクトリ構成を採用し、かつドキュメントごとにTopicPathが固定される状態であれば、かなり楽にTopicPathが生成できる。

これまではTopicPathはドキュメントあたりで設定することが勧められていた。これは、ACCS indexが組み込みで機能するためだ。

この機能を使えば設定ファイルによって、そのディレクトリの親パスを定義し、そして文書タイトルがStringとして追加される。

詳細

つまり、ディレクトリで設定されているのが

[ :Foo, :Bar ]journal/

で、文書タイトルがBazであるならば、

[ :Foo, :Bar, "Baz" ]

となる。

別にシンボルである必要はない。だが、シンボルを推奨している。マップを使用することが推奨されているためだ。
だが、特にシンボルであることを期待しているコードは組み込まれていない。

だから、例えば

[ { Address: "http://example.com/foo/", Title: "Foo" }, "Baz ]

のような構造にしても構わない。

実際にそれを使うコードとしては

% tp = DOC.mktopicpath
% tp.each do |i|
%   if i.kind_of?(String)
        <li><%= i %></li>
%   else
        <li><a href="<%= 	DOC.pbenv[:TopicPathMap][i][1] %>"><%= DOC.pbenv[:TopicPathMap][i][0] %></a></li>
%   end
% end if tp

のようになる。

ただし、Indexの場合はディレクトリで定義されている階層は今いるページなのであるから、その場合はtitleとpathの最終エレメントは重複しているはずだ。

そこで、:Indexが定義されている場合は、最終エレメントを取り除くことにした。

過渡期のコード

タイトルは"title"なのか"Title"なのか:Titleなのか、といったところに揺れがある。

正式には"title"を使用することになっている。だが、おそらくはreservedなキーは大文字で始めることにしたほうがいいだろう。

この互換性を維持するコードにしてある。

また、すでにページあたりでTopicPathを設定している場合に備え、現状はdevelブランチのみの対応だ。


途中で放置したため、書くべきことを忘れました。
ゴメンナサイ。

PureDocとKramdownのhn要素のidを統一する

「Markdownで書いたページがTOCのリンクが切れている」という問題に気づき、急遽対処を行った

問題は、Kramdownが生成するヘッダーIDが、単純にテキストを用いるものではなかった、ということだ。

KramdownはどうしてもIDにACSIIのみを使うようになっており、そのためになかなか複雑な処理をしている。

これを統一するため、Kramdownのこの処理をしているところを探したところ、base.rbにあるKramdown::Converter::Base#generate_idであることがわかった。

これを、常にテキストを用いるようにオーバーライトする。ほんとはオーバーライドしたかったのだが、Kramdownの構造的にそれは割と難しい。

PureBuilderのコードをいじれば、一応動くようにはなる。これは、そのままのテキストを使うようにして、PureDocと揃えたためだ。

Gist

PureDocのほうも複数の形式をサポート。その中でデフォルトは今までどおりテキストを使う形式だが、Kramdownに合わせ、重複した場合の対処を加えた。

これによってテキストだけではIDを判断できなくなったので、IDもTOCの中に含めるようにした。

Gist

はじめてのプルリク

libkkcのかなマップがShiftを押しているとアルファベットとなって変換が切られる、という問題、改善したマップが問題なさそうなので、アップストリームに投げることにした。

プルリク。手順としては

  • GitHubで対象のリポジトリでfork
  • 自分のところにリポジトリができるのでclone
  • ブランチを切る
  • 作業する
  • commitする
  • 自分のリポジトリ(origin)にpushする
  • この間にアップストリームの変更がない前提
  • GitHubで自分のリポジトリでブランチを作業したものに切り替える
  • Pull Request

これまで、パッチ投稿まではしたことがあったものの、プルリクはしたことがなかった。
それが上流に堪える品質で、かつ上流で反映されるべきものであるという確信がなければ、なかなかプルリクは出せない。

今回は、かなユーザーが少ないということもあり、結構自信があった。

そして、無事、マージされた!

これで数千人のFedora/libkkcユーザー(のうちの、かな入力ユーザー——10人くらい?)に貢献できただろうか。

サイト構築で画像関連機能

ウォーターマークをいれた画像を半自動で用意する

Gist

ウォーターマークのベースはさすがに自動生成できるレベルにはなかったので、Inkscapeで作成した。
縦と横の二種類を用意している。

fig/にはオリジナルサイズの、thumb/にはサムネイルサイズの画像を配置するが、この時にcompositeを使ってウォーターマークを合成している。

また、幅1600pxを越える画像はリサイズしている。

@figbaseuriの不完全さ

PureDocにもわずかな変更を加えたが、これはattrを排除するためにすぎない。

大きいのはParser::PureDocにおける

        # Other settings for PureDoc
        begin
        if config[:purebuilder_config][:puredoc_tune].kind_of? Proc
          config[:purebuilder_config][:puredoc_tune].call(::DOC)
        end
        rescue
          STDERR.puts "!!ERROR in PureDoc TUNE:" + $!
        end

という変更だろう。

これによって、@config[:puredoc_tune]を使って各PureDocオブジェクトに対して設定が可能になった。
これはMarkdownドキュメントに対しては無効となる。

Gist

「各article要素を取得してループ→各要素に対して各figure要素を取得してループ→イベントリスナー追加」という単純な構造ではある。

ライトボックス用に用意されたimg要素のsrcを変更し、さらに最大幅と最大高をウィンドウサイズに合わせてから表示する、という処理が追加され、ちゃんと機能するものになった。

figure要素に入っていないものはサムネイルの構造を持っていないとみなす方式。

新 Aki SI&Eサイト構築と、納得できないCSS解釈

作り直してるよという話はしていたが、ついに全面刷新となったAki SI&Eの作成に着手した。

昨日1日でlightboxプログラムを作り、さらにメニューボタンアニメーションを作った。

Lightboxアプリケーションは作った時点で1kBを切っており、相当軽量な内容になっていた。
JQueryなどは使っていない。

(function() {
  if (! document.addEventListener ) { return false; }
  
  var wrapper = document.getElementById("WrapWindow") /* ModalWindow */
  var fadingTimer = false /* IntervalTimer */
  var alpha = 0.0 /* ModalWindows's alpha number */

  /* draw content */
  var displayContent = function() {
  }
  
  /* fading out (interval callback) */
  var fadeout = function() {
    if (alpha < 0.8) {
      alpha = alpha + 0.05
      wrapper.style.backgroundColor = "rgba(0,0,0," + alpha + ")"
    } else {
      clearInterval(fadingTimer)
      fadingTimer = false
      displayContent()
    }
  }
  
  /* set this for event callback */
  var setLightboxTrigger = function(e) {
    wrapper.style.visibility = "visible"
    fadingTimer = setInterval(fadeout, 30)
  }

  /* Return from lightbox */
  wrapper.addEventListener("click", function(e) {
    if (fadingTimer) {
      clearInterval(fadingTimer)
    }
    wrapper.style.backgroundColor = "transparent"
    wrapper.style.visibility = "hidden"
    alpha = 0.0
  }, false)

  
  document.getElementById("SideNotes").addEventListener("click", setLightboxTrigger, false)


  
})()

基本的な作りはごく単純だ。
htmlbodyheight: 100%;を持っていて、

<section id="WrapWindow">
</section>

#WrapWindow {
  visibility: hidden;
  z-index: 10000;
  background-color: #000; /* for RGBa Unsupported browseres */
  background-color: rgba(0,0,0,0);
  min-height: 100%;
  min-width: 100%;
  position: fixed;
  top: 0px;
  bottom: 0px;
}

となっている。これにより見えていないし触れることもできないが見えている画面より手前に全体を覆うブロックがあり、これがvisibleになることで全体を覆って操作不能にする。

プログラム全体を関数で書こうことで、全体でアクセスできるプロパティを外側に伝播しないように閉じ込めることができる。「クロージャを作ることで変数をローカル化し直ちに呼び出す」というテクニックは、オライリーのJavaScriptで紹介されている。

グローバルな値としてfadingTimeralphaが定義されている。
fadingTimersetInterval()オブジェクトを格納するためのものだ。これをどこかにとっておかないとclearInterval()することができずに困ってしまう。

イベントが発生した時にfadingTimerが真か偽か、によって判断を変えることができる。
fadingTimerが真の状態で呼ばれる可能性があるのは、#WrapWindowに対するクリックイベントだけだ。
なぜならば、fadingTimerがセットされる前に透明なだけでvisibilityはvisibleになっているので、クリックイベントは必ず#WrapWindowが取る。もちろん、なんらかのブラウザの不備でクリックされる可能性はないではないが。
では、「これから真になる、まだ偽の状態で発生するか」というと、真になる関数が走った時点で、JavaScriptはシングルスレッドなので、真になるまで他のイベントを発生させても実行されないので関係ない。
操作に対するフリーズタイムを短くするためにsetTimeout()を使うこともあるくらいだ。

文字列(evalされる)の登録でなくコールバック関数の登録をするためには、intervalで呼ばれるコールバックで繰り返し使われる値を覚えておくことができないので、これはコールバック関数に対してグローバルでなくてはいけない。
そのための値がalphaだ。

1.0になれば終了なのでその時点でclearIntervalだが、まだインターバルタイマーが動作している状態で#WrapWindowがクリックされる可能性はある。これが真の場合だ。

では、その場合どうするかというと、インターバルタイマーの状態に関わらずオーバーレイを消す。そのため、インターバルタイマーが働いていればclearIntervalしてしまう。

なお、最後に追加しているイベントは本番用ではなくテスト用だ。

CSS

今回の目玉のひとつが、main, article, section, header, footer, asideといった「HTML5への対応」だ。
私のサイトにある大量のdivを少しでも減らしたいからなのだが、HTML5への対応は様々なメリットがある。
一方でレガシー環境を切り捨てることになりそうだったので慎重に対応してきたが、NN 4.6相当でも動作しそうな見通しがたったので採用となった。

それはともかく、納得できない振る舞いに随分悩まされた。

        <div id="SideContent">
      	<section>
             <nav id="Toc" class="toc marginbox_main contentbox">
               <h1>目次</h1>
               <ul>
                 <li>ページナビゲーション</li>
               </ul>
             </nav>
        ...
        </section>
        </div>

div, section, nav、あるいはそれらが内包する全てのテキストに対してfont-sizeの相対的変更(80%など)をすると、ボックスが上下とも縮まり、左カラムと位置が揃わなくなる。
インスペクタで見ると、table-cell要素の#SideContentの位置は正しいが、sectionのマージン上辺と#SideContentの上辺の間に隙間ができる。

また、中に入っているボックスの上マージンを削ると、ボックスの高さがそれだけ減ってその分上に隙間が空く。
配置は関係ないらしく、またこれはpaddingが設定されている場合のみ発生する。
paddingを設定したボックスに内包されるブロック要素のmarginをいじるのは、高さを意識する必要がある場合は厳禁ということか。

        <section id="MainArticle">
          <article class="marginbox_main contentbox">
            <h1>記事のタイトル=章</h1>
                ...
          </article>

このh1のマージンを設定するとその分articleが下がる。
h1より上にボックスを置くとボックスは伸びるが、その分右カラムが下げられてしまう。

恐らくは「h1を特別扱いしている」のだろうと思う。
だが、このためにh1を修飾することが非常に難しい。

結局position: relativeにしてずらした。上以外のmarginについてはこうした問題はない。

携帯万能のバグに対応する

携帯万能で、WX11Kの送信メールを取り込むと文字化けの上無効な引数が発生しましたと表示される問題がずっと出ていた。

この問題、先日対応したつもりだったが、甘かった。

そもそも問題は「WX11Kのメール」ではなく、「UTF-8のメール」にあった。
UTF-8で送られると、その時点でアウトなのだ。そのため、事前に問題のあるエントリを切り出しておく、というのは労力が大きい。

そこで自動判別できるようにしたのだが、新たな事実が判明した。

だが、新たな問題が発覚。
CSVだが、"をエスケープしていないため、Invalid formatとなっている。
このため処理できない箇所があり、力技でなんとかした。

このあたりはGitHubでスクリプト公開と共に解説している。

問題はPLANEXサポートに報告済み。
8時間以上もかけたのだが、甲斐ない結果に終わった。

Markdownで書かれたスライドをHTMLとポータブルPDFと印刷用PDFにする

Aki SI&EのPR用に書いたものだが、Markdownで書き、Web(HTML)、配布用PDF、印刷用PDFの3種類を生成する考えでいた。

手でやろうかと思っていたのだが、再生成の回数も多そうだったので、自動化できるようにした。

Markdownで記述したドキュメントと、オリジナルサイズの画像ディレクトリ、mogrify -geometry x300でリサイズした画像ディレクトリが用意されている。
Web用のものについてはHTMLで出力し、リサイズされた画像のWebサーバー上のコピーを参照しなければならない。

PDFについては、MarkdownをLaTeXに変換してという方法もあるにはあったが、画像を含めた細かなデザインを施したかったため、HTMLをベースにPDFに変換することにした。

MarkdownコンバータはRubyのKramdownを使用。改ページはないため、wkhtmltopdfを用いてPDFに変換することとした。
参照しているディレクトリの違いで2回ループを回す。

それぞれHTMLは別に出力し、テンポラリファイルとして出力したHTMLを元にPDFのテンポラリファイルを生成、unitepdfを使ってこれを結合する。

専用のコードなので多くをハードコーディングしているが、これで正常に機能する。

#!/usr/bin/ruby
# -*- mode: ruby; coding: UTF-8 -*-

require 'kramdown'
require 'erb'

def treat_md(file, findex)
  $article_body = doc = Kramdown::Document.new(File.read(file)).to_html
  
  $article_title = doc[/<h1[^>]*>(.*?)<\/h1>/, 1]
  
  foot = Array.new
  if findex > 0
    foot << '<a href="' + ( SOURCEFILES[findex - 1].sub(/\.md$/, '')  + ".html" ) + '">前へ</a>'
  end
  
  foot << '<a href="/si/" target="_top">Aki SI&Eトップページへ</a>'
  
  if findex < ( SOURCEFILES.length - 1 )
    foot << '<a href="' + ( SOURCEFILES[findex + 1].sub(/\.md$/, '')  + ".html" ) + '">次へ</a>'
  end
  
  $article_footer = foot.join
end


TEMPLATE = <<'END'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" http://www.w3.org/TR/xhtml1/dtd/xhtml1-transitional.dtd">
<html>
  <head>
    <title><%= $article_title %></title>
    <meta http-equiv="content-language" content="ja" />
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <style>
body {
% case $mode
% when :Web
  font-size: 130%;
  color: #333;
  font-weight: bold;
  margin: 0px;
  padding: 0px;
% else
  font-size: 18pt;
  color: #000;
  font-weight: bold;
  margin: 2cm auto;
  padding: auto;
% end
  background-color: #fff;
  font-family: "Migu 1C"
}   

h3 {
  font-size: 135%;
}

#Container {
  margin: auto auto 100px;
  max-width: 950px;
  text-align: center;
}

img {
% case $mode
% when :Web
  height: 300px;
% else
  height: 18em;
  max-width: 100%;
% end
}

ul {
  text-align: left;
  margin: auto 1.8em;
}


#ArticleFooter {
% if $mode != :Web
  display:none;
% end
  position: fixed;
  bottom: 0px;
  margin: 0px;
  width: 100%;
  max-height: 100px;
  background-color: #339;
  color: #fff;
  text-align: center;
}

#ArticleFooter a {
  padding: 0 0.8em;
  text-decoration: none;
  color: #fff
}

#ArticleFooter a:hover {
  text-decoration: underline;
}
    </style>
  </head>
  <body>
    <div id="Container">
    <%= $article_body.gsub(
      %r{src="img/},
      %(src="#{$img_type}_img/)
    ) 
    %>
  </div>
    <div id="ArticleFooter">
      <%= $article_footer %>
    </div>
  </body>
</html>
END

$article_body = nil
$article_footer = nil
$article_title = nil

SOURCEFILES = Dir.entries(".").select {|i| i =~ /\.md$/ }.sort

# HTML
$img_type = "/img/si/intro_slideshow/resized"
$mode = :Web

SOURCEFILES.each_with_index do |x, i|
  begin
  treat_md(x, i)
  File.open("out/#{x.sub(".md", ".html")}", "w") do |f|
    f.puts(ERB.new(TEMPLATE, nil, "%<").result)
  end
  ensure
    $article_body = nil
    $article_footer = nil
    $article_title = nil
  end
end

# Web PDF
$img_type = "resized"
$mode = :PDF

SOURCEFILES.each_with_index do |x, i|
  begin
  treat_md(x, i)
  fn = "out/_tmp_#{x.sub(".md", ".html")}"
    File.open(fn, "w") do |f|
      f.puts(ERB.new(TEMPLATE, nil, "%<").result)
    end
  system('wkhtmltopdf "%s" "%s"' % [ fn, fn.gsub(".html", ".pdf") ])
  ensure
    $article_body = nil
    $article_footer = nil
    $article_title = nil
  end
end

system 'pdfunite out/_tmp_*.pdf out/portable.pdf'
system 'rm out/_tmp_*'

# Printable
$img_type = "original"
$mode = :PDF

SOURCEFILES.each_with_index do |x, i|
  begin
  treat_md(x, i)
  fn = "out/_tmp_#{x.sub(".md", ".html")}"
    File.open(fn, "w") do |f|
      f.puts(ERB.new(TEMPLATE, nil, "%<").result)
    end
  system('wkhtmltopdf "%s" "%s"' % [ fn, fn.gsub(".html", ".pdf") ])
  ensure
    $article_body = nil
    $article_footer = nil
    $article_title = nil
  end
end

system 'pdfunite out/_tmp_*.pdf out/printable.pdf'
system 'rm out/_tmp_*'

完成したものについては

Google DOCs Viewerはスライドショー向きではないので、ウェブ版を使うか、PDFをダウンロードするのがお勧め。