ウェブサイト全文検索システムの開発

開発経緯

NamazuやGroongaも試したのだが、いまひとつ望むものにはならなかったので、シンプルで美しい全文検索システムを書くことにした。

これは、第一にはGoogle検索に依存しているMimir Yokhamaの検索機能を自前で持つこと、 第二にはChienomiを含むWordPressのシステムの置換えである。

検索システムの開発自体はそれほど難しくないと思うのだが、どのように動作するのが望ましいかということを考えると非常に難しい。 Googleの検索システムは非常に高度なので、それに匹敵するものを作るのは難しいのだ。

だが、ここはPureBuilder Simplyにふさわしいシンプルなものを目指すことにする。

設計その1

とりあえず、grepを使えば話が早いのだが、HTMLだと余計な要素を含んだ検索になってしまう。 HTMLからタグを除去するのは難しくないが、どういうポリシーで除去するのか、いつどうやって除去するのか、などが難しい。

PureBuilder Simplyの構成から言えば、生成時に、Pandocで生成するのが望ましい。

$ pandoc index.md -t plain index.txt

だが、生成時に生成を全く無視してインデックスを生成するのはどうだろうか? そもそもPre pluginsはソースファイルを、Post Pluginsは生成したHTMLファイルを加工するものであるため、本来の目的から逸脱してしまう。 例えばPre Pluginsを使って

とかもできる。

ただ、今のところ検索対象になるようなソースファイルを加工するようなPre plguinsを使っていないため、別にこのようにする必要性はない。

設計その2

あとから処理するためのもの。 .indexes.rbm に基づいて処理を行う方式。

検索

いずれにせよここまでやってしまえば検索は簡単。 grepで検索できる状態なので、シンプルに検索可能。

AND検索の要領としては

ものすごく検索対象が多い場合は、検索対象そのものを絞り込んでいくほうがいいだろう。 だが、プロセス起動回数が増えることを考えると、そのような場合は自前実装のほうが良い可能性が高い。

OR検索はもっと簡単で

スペースの取り扱い方とか、case問題とか考え始めると難しい。 ただ、世の中そんな複雑な検索をしている検索エンジンはあまりないし、多分ローカルにそんなもの作ったところで報われないのでこれくらいでいいような気もする。

ANDまたはORではなく自由にANDとORを結合できるようにした場合は、expr自体に評価できるメソッドを追加すると良い。例えば

といった感じである。

あとがき

検索機能の実装自体は難しくないのだが、ChienomiをPureBuilder Simply化するという話になると結構難しい。

既にかなりの記事があり、検索からの流入も多いため、どうしても全記事に対してマップせざるをえない。 これもなかなか面倒だ。

だが、もっと問題Chienomiの記事は書き方が一定でない、ということだ。

Chienomiの記述形式はPOD, RDoc, ACCS2, PureDoc, PureDoc2, PureDoc2::Markdown, Pandoc Markdownがある。 PureBuilder SimplyはPandocでの処理を前提としているため、なんとかしないといけない。

過去記事については諦める方針ならばHTMLとして抜き出すという方法もあるのだが(Mimir Yokohamaでウェブサイトのサルベージでよく使う方法だ)、 できれば避けたいというのもある。

また、タグとカテゴリのつけ方が一定ではないため、これを処理しなければならない。

さらに厄介なのがメディアファイルだ。 WordPressはメディアファイルの使い方が独特だし、そのためにメディアファイルについてはWordPress上で追加する方法をとっていた。 さらにサイト移行時にメディアファイルを紛失したこともあって、結構大規模な作業になると思う。

そのことを考えると一筋縄ではいかない。

それはともかくとして、サイトの検索で非常に複雑な演算子を使いたがる人はまずいない、 どころかサイト内検索なんてほぼ使われていないに等しいので、基本的な検索機能で十分だと思うのだが、それであればこの通り実装はとても簡単だ(例によって設計で稼いだ感があるが)。

というわけでちょっとした実装例、そしてシェルスクリプトサンプルとして役立てば幸いである。

Mimir Yokohamaで続く改修、力を注ぐ

2018年8月5日、Mimir Yokohamaのソースリポジトリにはじめてのタグ、5.0が打たれた。

なぜはじめてのタグなのに5.0だったのか。

これは、Aki SI&Eのプロトタイプを1.0, Aki SIEのウェブサイトを2.0, Mimri YokohamaのWordPressを3.0, 現在のウェブサイトになったときを4.0とカウントしたものだ。

つまり、はじまって以来の、同一システムのままのバージョンアップとなった。

しかもこのタイミングである。 先日、大幅な編成変更と、いいね機能、コメント機能の追加を行った。 これで新しい船出だというタグではない。かといって同バージョン最終仕様としてのタグでもない。

これから次々とアップデートが予定されている。 その中での一区切りだった。

13年積み上げてきたものとは

PureBuiler Simplyの原型となっているのは2005年のACCS1である。

もうブログが流行り始めていた頃だったが、既に「事前生成戦略」に関するイメージはあった。 ブログを避けた理由は、一連の流れを持つ記事群を拾いにくくなること、そして時間とともに消えてしまうことだった。

「普遍的な内容を、きちんと分類して読みやすいように提供したい」と考えたわけだ。

様々な機能、様々なアイディアがあった。 実装されたものもあるし、実装されなかったものもある。 言語もPerl, PHP, Zsh, Rubyと変わってきた。 このような変遷をたどっているのはEQAIとこれだけで、まさに私のライフワークであり、また成長の軌跡でもあった。

この中でこだわってきたものもある。

例えば、デザイン性を保ったまま軽量・高速なウェブサイトを構築すること、可能な限り高いアクセシビリティを提供すること(環境や回線の違い、身体的ハンディキャップなどで差別しないこと)などもそうだ。 意味あるコンテンツを、読みやすい形で提供する、というのもある。

ACCSが追い求めてきた機能は次のようなものだった。

  • カテゴリで分類されて探しやすいインデックス
  • 検索機能
  • 意思表示機能 (コメント、と考えていることが多かった)
  • 連続した記事のページめくり
  • 要約の先読み
  • インライン用語集
  • prev, next, glossary, index, description情報を持たせる

PureBuilder Simplyは当初、Mimir YokohamaのWordPressページで提供されている全機能を提供する、ということを目標としていた。 これについては既に達成されている。そのために、従来のPureBuilderにはなかったタグ機能なども追加された。

「いいね機能」「コメント機能」はWordPressに完全な意味で標準であるものではないのだが、事実上付属するようなもの(特にコメント機能は)なので、これを追加したことで、事実上「WordPressを置き換える」というミッションは完遂した。

だが、同時に既にWordPressにはない機能の搭載もはじまっていた。 用語集機能はWordPressではなく、PureBuilderの伝統に由来する。そう、ACCSが目指していたものを達成する、という次のミッションに向かいはじめたのだ。

そこでつけられたのが5.0タグだった。

用語集機能

用語集機能という発想のスタート地点は、私が使っていたWindows 98SEマシンに搭載されていたインライン翻訳ソフトだった。

カーソルを載せるだけでその単語を翻訳してくれる、というソフトウェアは今持ってそれ以上のユーザービリティを提供するものはない。

「カーソルを載せたらわからない言葉を教えてくれる」というのは最高に便利だと思ったのだ。 現在はニコニコやはてなが似たような機能を提供しているが、あれはページが変遷してしまうため私からすれば理想的ではない。 どらちかといえばWikiに搭載されているインライン展開のほうがずっと理想的だ。

この機能は

  • 表示後にJavaScriptでtreatする
  • 生成時にHTMLを置き換える形で組み込む
  • ソースドキュメントを改変してから生成する

という3つのパターンを行ったり来たりしている。

PureBuilder SimplyではPost Plugins/Pre Pluginsの構造からこの3つとも選択肢として取ることができる。 Mimir YokohamaではPost PluginによるHTML置換え方式を取っている。

このあたりは試行錯誤の成果といえるだろう。 用語集ページも含めてYAMLの辞書ファイルから生成しており、文書に対して特別な処理は必要ない。

次の記事、前の記事

ACCSで最も苦戦したのがこの機能だ。

この解決については何度か言及しているが、今は前後関係をメタ情報として書く、という仕様になっている。

解決方法としては後退しているように見えるが、前後関係の自動解決はファイル名なり、もしくはなんらかのヒントなりを厳密に管理する必要があり、結構バグりやすいということを経験したのだ。

複雑な置換えやユーザーの管理によってバグを発生しうるようなものであるならばメタ情報をユーザー自身が書くべきだ、という割り切りは、これまでの歴史の中で手にしたバランス感覚である。

実のところ今の構成では前後を自動化することは難しくない。 だが、ユーザーがそれを守ってくれることを期待すべきではないだろう。

Post pluginsはページ生成が終わってから実行されるが、これは「他の記事の情報を取得できるようにする必要がある」からであり、 環境変数$pbsimply_indexesという形でデータベースのパスが渡っていることから

のようにして取得できるし、 連番に限るのであればもっと簡単に

とできる。 ちなみに、ディレクトリを認識させる方法は最新のコミットで環境変数$pbsimply_subdirに含まれるようになった。

このように「できるけれど、あえて手書き」だ。 これは、問題を簡単にするためと、この処理をPandocテンプレートを通じて行いたいためだ。 Pre Pluginsを使えばできるが、かなり複雑なことをすることになるため避けている。

意思表示機能

過去には「コメントを直接にHTMLファイル化し、objectで読ませる」ということをしていたこともある。

表示するかどうかを別として、コメント機能はそのときとあまり変わっていない。 表示させるために必要な部分を削ってシンプルになったくらいだ。

このような機能は本質的な部分は極めて簡単に書けることはこれまでの経験によって証明されている。 どの程度正当性を検証する必要があるかという点がwebアプリケーションの分量になる。

要約の先読み

まずは要約を入れる

いよいよ今回のハイライトだ。

descriptionへの対応自体は最初のリリース時点で

という記述があり、対応はちゃんとしていた。 だが、「書くのが面倒」「書いてもあまり意味がない」ということで放置していた。

「descriptionの先読み」は今まで実装計画には入っていたが、実装されたことはなかった。 そもそもdescriptionってSEOのために入れられているくらいで、「descriptionを読ませる」という発想はあまりない。

Firefoxだとこんなふうにブックマークのプロパティを表示するか、ブラウジングライブラリー上で詳細表示にするとdescriptionが表示されたりするのだが、これを見たことがある人という人は地球上に5桁いないのではないだろうか。

Firefoxのブックマークの詳細

だがdescriptionは入れたいと思っていたし、それを活用したいと思っていた。

そもそもの発端はトップページのレイアウト更新で、最新の更新記事と要約を(ニュースとは別に)表示したい、ということだった。 「どうせ記事の要約書くんだったらdescriptionに入れようよ」ということだ。

実はAtomフィードもスタンバイしている。

要約を見える形に

だが、これだけではおもしろくない。どうせ要約を表示するのならばぜひともユーザーに見える形にしたい。

私の文章は基本的に長いので(体系的でない短い文章に価値を感じていない)、読むのがしんどい人もいるだろう。 読むかどうか決めるために要約は重要だ。

要約を見たいタイミングとはいつだろう? やはり記事を読み始める前だろう。ならば本文前に

とかやってやればいいし、そのほうが効果的なのかもしれないが、既に「文書情報」という項目があることを考えるとちょっといただけない。

そこで文書情報に追加した上で「記事タイトルをクリックすると文書情報にジャンプする」という仕様にした。

これはヘルプページにも書いてあるけれども、誰も気づかなそうだ…

もうひとつ、利用者は多くなさそうだが、カテゴリインデックスがある。 ACCSとしてはこれが中心であり、ぜひとも使って欲しい機能だ。 世の中、情報を整頓するということに怠けすぎて、検索が全てになってしまっているので、使われていないような気もするけれど…

しかし私の意図としてはこのようなインデックスを活用してほしいというのがあるし、やはりタイトルだけではわかりにくい。 かといって変遷するとだるいので、変遷せずにインデックス上で要約を確認できると便利だ。

これはタイトルで関心をそそられた後の二次的な情報であり、通常は一覧性が高いほうがいい。 というわけで、ツールチップにしてみた。

PureBuilder Simply ACCS上でDescriptionを扱う

単純には記事タイトルにtitleで入れてあるため、ロールオーバーツールチップとして表示される。 だが、スマホだとこれが効かないので、補助的に“📖?”と表示して、これをタップすればツールチップが表示されるようにしてみた。 全く標準的でないインターフェイスなので、あまり気づいてもらえないような気もするけれど…

PBS ACCS用ツールチップ実装

何度見てもJavaScriptの複数代入が慣れない。 慣れればみやすそうだけども。

世の中的には割と珍しいDOM操作をしているが、これはelementに対してイベントリスナを設定するためで、innerHTMLだと二度手間になる。 基本的にやっていることは「記事部分 > リスト全体 > リンク」と絞り込んでいって要素を作成して追加する、という手順だ。 末っ子要素を追加するとき(今の要素の親要素の最後の子要素として追加する)はelement.parentNode.appendChildという手順は覚えておいてもいいかもしれない。

glosarryと共通のコードがライブラリとして読まれるようになっている。ライブラリはdeferだがasyncではない。

900pxを堺にしているのは、「サイドカラムがあるのであれば表示領域は少なくとも右側にサイドカラム幅はあるが、シングルカラムになるとそうではない」からだ。(このページのシングルカラム境界は800pxである)

機能チェックしているが、document.addEventListenerできないブラウザでJavaScriptに対応しているものはあまり残っていないだろうし、あっとしても単純にイベントリスナー設定時にエラーになるので放置してもいいかもしれない。 ただし、間違って複数回ライブラリが読まれたときのためにArt.tooltipはしておかないとイベントリスナが複数設定されてしまう。

PureBuilder Simplyはうまくいっている

PureBuilder Simplyがここまでうまくいっている理由としては、やはりPandocの強力さがなによりだろう。

PureBuilder SimplyはPandocが持っている機能をちょっと拡張する…という考え方をしている。 今までドキュメントジェネレーター自体を制作していた(PureDoc)ことと比べると問題はかなり簡単になっている。

Pandocの動作は必ずしも簡潔ではなく、自分で実装するのであればドキュメントジェネレーターにここまでの機能をもたせることはないだろう。 だが、Pandocがある以上はPandocを使いたい。

もしPandocがなければdocutilsを拡張することを考えただろうが、その場合はPureBuilder Simplyは今のように良いツールにはなっていなかっただろう。

PureBuilder Simplyが今ほど素晴らしいツールになったのは、Pandocがあったからこそだ。

Mimir Yokohamaに対して行われている様々な拡張は今の所PureBuilder Simplyに対して適用されていない。 これは、PureBuilder Simplyが生成するものに対する機能ではなく、Mimir Yokohama固有の、そしてテンプレートとCSSによるものだからだ。

だが、いくらかでも一般化してPureBuilder Simplyに還元していければと思っている。 PureBuilder Simplyのエコシステムの充実は普及には不可欠だろうから。

Mimir Yokohamaに「いいね機能」「コメント機能」を追加

概要

Weekly 10000PVを達成して機能強化に力の入っているMimir Yokohamaのウェブサイト。

連続の機能強化でついに「いいね機能」と「コメント機能」が追加された。

実は先日の「お問い合わせフォームの実装」は単にその機能を実装する最小限ではなく、簡単なアプリケーションを実装できるプラットフォームになっており、 アプリケーションを追加する条件が整っていたのだ。 また、そのためのテストもしてあった。

そのため、実は今回コード追加はわずかで、両方合わせても23行ほどにとどまる。 ごく簡単だが、確証が持てないためにテストと本番環境のための修正を行ったりして結構な時間がかかった。

これに関してはみるべきところはあまりない。 受け取ったパラメータをファイルに書き込めば良いだけだからだ。

ちなみに、連打しやすいアプリケーションを入れるために連打の対策もサーバーにしてあった。 実は先のパフォーマンスチューニングはこの対策によってパフォーマンスが低下してしまったため、これをカバーするついでに行ったものだった。

いいね機能の設計

いいね機能はごくシンプルだ。

Pandocテンプレートを使ってページタイトルを埋め込むことができるので、これだけ使うのであれば単にテンプレートの中にフォームを書けばいいだけ、ということになる。

実際はリファラ(Rack::Request#referer)及びユーザーエージェント(Rack::Request#user_agent), IPアドレス(Rack::Request#ip)も特定に使用している。

ポイントは「一度送信したら表示を変更、送信を無効化する」ということだ。

inputタグのdisabled属性を使うことで送信ボタンを無効化している。

そう、この機能はHTML上でインラインで書かれているのだ。 このような書き方はW3C的には推奨されないのだが、Googleは推奨している。 実際、これだけのためにJavaScriptファイルをロードさせることをしたくなかったので、インラインにした。

ポイントは

  • Legacy DOMにおいてフォーム部品は連想配列のようにアクセスできるようになっている
  • submitボタンのラベルはvalueである
  • disabledによってフォーム部品を無効化できる

である。

これらの処理は送信の「前に」行われる。 これは正しいことではないが、問題はない。 なぜならば

  • 送信できなかったからといってユーザーが修正するなどの手を入れる余地はない
  • 特に返信を必要とするものでもないので、送信失敗はクリティカルな問題でもない
  • サーバーエラーなどは表示されるようになっている

からだ。

また、画面変遷せずに送るだけ…というと、Ajaxで非同期に送るしかないように考えるかもしれないが、 実際は現代のブラウザは基本的に2XXステータスで空コンテンツを返すとページ変遷しないようになっている。

このような用途のために204(No Content)が用意されているため、204を返す仕様だ。 できるだけブラウザの標準機能に頼るようにしている。

このボタン、めっちゃgooglebotが押してくる…

コメント機能の設計

公開されるものではなく、sanitizeしなくても問題が発生しない設計になっている(単に文字列として扱う以上の取り扱いがなされる条件で使用しない)ため、非常に楽だ。

難しく考えるよりも、シンプルで挙動をちゃんと把握できていて、余計なことをしない方法をとるのが最も楽に、確実に、バグなく設計できる。

ただ、この機能に関しては「コメントフォームの表示」などが必要になり、JavaScriptが必要になった。 また、コメントするというあまりとらない行為のための部品であり、常にロードすることは非常に好ましくない。

そこでとった方法は「JavaScriptの遅延ロード」であり、「クリック時にelementを追加してJavaScriptを読み込む」という方法だ。

これでscript要素を追加している。

では呼び出されたあとどうしているのか…というと、こんな感じだ。

そう、コメントフォームは標準でHTMLに含まれていない。

パフォーマンス的にみても親切機能のためにもう重くなりつつあるドキュメントをこれ以上重くしたくないので、 「フォーム部分はJavaScriptがロードされたときにinnerHTMLで書く、という方法をとっている。

「クリックされたときにはじめて必要とされるのでドキュメントノードを追加してロードする」という手法は稀に使われるが、さすがにそれでHTMLドキュメントそのものを生成するというのはまず見ない手法になっている。

連打されたときのことを考えて、clickイベントを発生する値に特殊なプロパティを埋め込み、何度も実行されないようにしている。 なお、これは本当に連打したときだけ機能するもので、JavaScriptシングルスレッドなので一回このスクリプトに入っていまえばこのスクリプト中に割り込まれることはない。 だが、連打されるとイベントがキューに入ってしまうので何度も実行されてしまうことから、そうしたことがないようにしている。 シングルスレッドなので、このスクリプトが実行されているのにプロパティが設定されないうちにまた実行されるということはない。

元になるフォーム自体は存在していて、submitボタンを配置すればフォームの送信は可能だ。 そのため、submitイベントに対するイベントリスナーを設定するものはHTML上に静的に存在している。

その上で「フォームはやっぱり閉じられたほうがいいな」ということでボタンクリックに対するイベントの変更、及びボタンラベルの変更を行っている。

送信を行った場合はもうコメントフォームは使わないので、コメントフォームは非表示にして(削除はしていない)ラベルを変更している。

ラベル変更だが、input部品とは違ってbutton要素は子要素テキストノードがラベルになっているので、dataプロパティの書き換えによって変更している。

もともとHTML上でLevel0 DOMイベントを使っているので、スクリプト上でもLevel0 DOMによってイベントを変更している。 いつもaddEventListenerを使っていたので、珍しい。 イベントの削除はイベントにnullを代入するだけだ。

なお、HTMLで全て含めてしまえばJavaScriptは排除できるのだが、これ以上余計な要素を組み込むことはできれば避けたいためこのような仕様になっている。既にDOMコンパイルは割と重い。

また、デザインポリシーからいって、多くの場合余計なコメントフォームを常に表示させておくというのは美しくないとも思っている。 JavaScriptを使わずCSSでオンオフするようにもできるが、そうすると今度はボタンのテキストを変更するのが難しいし、連打や再送信を防ぐのも難しい。

このことから、なにがなんでもJavaScriptを使わないのが正義、ということでなければ、 このような付加コンテンツはユーザービリティの点からも素直に使うべきだと思う。

こちらもいいね機能同様、送信する前に状態を変更してしまい、成功すれば204を返す仕様。

Legacy DOM と DOM Level0 Eventに関して

Legacy DOMやDOM Level0 Eventについて意見をもとめられることがたまにあるのだが、私としてはあまり勧められないと考えている。

非常に簡単なので学習にはいいが、Legacy DOMはHTMLの構造に依存する。例えば

として、

とかやってしまうと、ドキュメントの構造が変わるたびに修正だ。

だが、「フォームにのみnameで使い」「フォームはW3C DOMで特定する」のであれば悪くない。

DOM Level0 Eventはイベントリスナーが1つしか登録できないためモジュール設計になっている場合や、なんらかのライブラリを使っている場合は使ってはいけない。 イベントを「追加」する場合はLevel2で、イベントを「設定」する場合はLevel0という使い分けも考えられる。 HTMLに直接書く場合はLevel0しか書けないので、その部品の基本的な動作と定義されているならLevel0でもいい。

ただし、その場合でもできればLevel2 Eventを「後から追加する」のが適切なので、DOM Level0 Eventの使いどころは今回のようなものが唯一だと思う。

メッセージフォームのサポート (Nginx + FastCGI + spawn-fcgi + Rack + Ruby)

あらまし

Mimir Yokohamaでついにお問い合わせ方法として「メッセージフォーム」が追加された。

なにがついになのか、なにをドヤっているのかと思うかもしれない。 まぁ、ドヤってはいないのだが。

実は私はかなり長い間ウェブアプリケーションをほとんど作っていない。 そして、今まで私が作ったウェブアプリケーションは、専用サーバーを持つサーブレットタイプか、もしくはCGIだった。

馬鹿にされがちなCGIだが、利便性は高く、頻繁にアクセスする性質を持たないアプリケーションには適している。

そして、そもそもウェブアプリケーションを作っていなかったのは、私が「事前生成戦略」の研究と実験に注力していたからで、 どちらかといえばウェブアプリケーションからは離れる方向にあった。 そして、ウェブアプリケーションを必要とするとしても大部分は静的ページとして提供できる方式を目指していたため、CGIで十分事足りたのである。

ちなみに、これまでウェブサーバーは

  • Apache
  • lighttpd
  • delegate
  • Nginx

という経過をたどっている。 Apacheは言うに及ばずlighttpdとdelegateはApacheよりもCGIが簡単だったので、「ほぼCGI」だった。

だが、時代は変わった。NginxはCGIをそもそもサポートしない。 私も新しい時代に対応する必要がある。

ちなみに、この作業は次の仕事のための実戦テストという意味合いもあった。

方針を考える

最も話が速いのはFastCGI Wrapである。

NginxはFastCGIをサポートしている。 FastCGIはプログラムをデーモンのように起動しっぱなしにする。

だが、通しで実行するプログラムとデーモンではそもそもの前提が違う。 そのためCGIプログラムをFastCGIとして動かすのはそれなりにハードルが高い。

そこでFastCGI Wrapの登場である。 FastCGIとして利用されるプログラムをFastCGI Wrapにする方式だ。 このラッパープログラムは要求に合わせて都度CGIプログラムをCGIインターフェイス経由で起動する。 結果的にFastCGIの意図は無視して従来型CGIを動作させるようにするというものだ。

この方法は結構出てくるのだが、基本的には既存のCGIプログラムを動作させる話である。

個人的な感覚としては、無駄なプロキシを噛ませるような方法を使ってまでCGIに固執したくない…というか、実はfcgi-wrapってそれなりにめんどくさい。

だったらFastCGI直というのもありかなぁ、と考えるわけだ。

ところが、やっぱりFastCGIはデーモン状のプログラムを想定しているわけで、やはり前提が違う。 要求として割と複雑なのか、デーモン化に関してはspawn-fcgiに担ってもらって、さらにRackを使う、というのがどうやら主流らしい。

だいぶ話が複雑になってきた。

サーバーはNginxである。NginxはFastCGIインターフェイスを経由してFastCGIプログラムにパラメータを渡し、応答を受け取る。

FastCGIプログラムはデーモンである。 Rubyでは次のようにしてFastCGIプログラムを書くことができる。

あるいは、CGIライブラリ互換インターフェイスを使うことで、#each_cgiの中身はまるっきりCGIと同じにすることもできる。

spawn-fcgiはこのデーモン部分を担う。 つまりeachしてる部分を担ってくれるわけだ。

プロセスとしてCGIインターフェイスで起動するわけではないので、fcgiwrapほどの互換性はない。 感覚はCGIに近いが、インターフェイスは意識する必要がある。

Rackはミドルウェアと呼ばれている。これはまずFastCGI抜きで話そう。

Rackはインターフェイスを担っている。 今までプログラムはCGIなり、あるいはFCGIなり、さらには各種フレームワークやサーブレットの様式(例えばSinatraとか)で書いていた。

Rackはこれらの違いを吸収するモジュール設計のものだ。 Rackに準拠したプログラムを書いておけば、たとえ愛用のフレームワークがディスコンになっても、サーバーが変わっても安心、というわけだ。

だが、Rack自身はサーバーではないからサーバーがいるのだが、Rack組み込みのサーバーというのはもう完全にRuby世界の住人だ。 だってRackはRubyのWebアプリケーションインターフェイスだから。

Passengerというソフトウェアがあって、これはwebサーバーのモジュールとしてRackに対応する。 Apacheでは比較的簡単だけれど、Nginxだと結構きつい。

そこでRackに対応したサーバーを立ててサーバーとサーバーでやりとりさせる、という方式がすごく現代的。 直接にRack経由でプログラムとやりとりするのはRackに対応したサーバーだけれど、Rackに対応したサーバーにwebサーバーとしての機能を持たせると大変なので、「本物のwebサーバーに矢面に立ってもらって、RackサーバーはあくまでRack対応に特化」というわけである。

Rackに特化したサーバーとしては(別にRackだけではないんだけど)、Webrick, Mongrel, Puma, Thin, Unicornあたりがある。

しかしRackでやりとりする方法があればいいので、FastCGI + Rackという方法もある。 それはRack側でFastCGI経由で受け取って、応答するためのハンドラが用意されている。

つまり、Unicornのようなサーバーを立てる代わりの手段としてFastCGIが使える。 FastCGIもデーモンを必要とするので別にFastCGIにすることで間に挟まってるものを減らす効果はない。 ただ話が楽になるだけである。

Unicornはむちゃくちゃ速いので、UnicornでUnixドメインソケットを使えば形式とししてはspawn-fcgiでUnixドメインソケットを使っているのと一緒だし、やっていることははるかに高度になる。 これが超モダンなやり方である。

が、あえてのFastCGI。 理由は管理する要素数を減らすためである。必要がないのにいかついものを使うことはしない。 これはサーバー運用のコツでもある。

なお、Rackに関してはかなり情報が少ない。 なんらかのフレームワーク…というか、ほぼRailsのバックエンドとしてのRackの話だけで、Rack単独の話ってない。 そして、FastCGIを使う話もない。これもだいたいなんらかのアプリケーションが「使ってる」あるいは「使わせる」話になる。

なんというか、みんなそんなに自分でプログラム作るってことをしてないのか… 世の中エンジニアたくさんいるのに、WordPressとRailsだけで満足なのか…

そんなわけで情報が猛烈に足りていない中、FastCGIとRackについて勉強することになったわけだ。

なお、Nginxでアプリケーションとやりとりする方法に関してはDiscourceで散々やったので経験済みだ。

なぜRackなのか

もちろんこのことからもわかるようにRackはなくても構わない。 spawn-cgiも使用せず単独のFastCGIアプリケーションを開発するのは容易である。

私が気にしたのはRubyのfcgiライブラリは2013年から更新が止まっているとい点だ。 また、Arch LinuxではfcgiライブラリはAURにもなく

# gem install --no-user-install fcgi

とするよりない。

ベーシックな機構であるFastCGIそのものが廃止になるようなことは考えにくいが、NginxのCGIの扱いのように消極的なサポートへと変遷する可能性はある。 その場合にアプリケーションの書き直しが発生してしまう。

Rackは現在主流であり、新規採用例も多い。 Rackが廃止になると影響を受ける範囲も非常に広いので今後10年は安泰だと思われる。

そこでFastCGI+Rackという構成にしたわけだ。 この場合でもRackはFastCGIをネイティブサポートしているわけではく、fcgiライブラリを使ったハンドラを同梱しているだけなのでfcgiライブラリは必要となる。実はこれを回避したかったのだが、結局はできなかった形だ。

とはいえ、この状態であればFastCGIを捨ててUnicornに移行するのも難しくはない。

とりあえずやってみる

Nginx

location / {
    root /var/www/testapp;
    fastcgi_pass /var/run/fcgi-testapp.sock
    fastcgi_index testapp.rb;
    include fastcgi_params;
}

Rack Application

Requestのほうはインターフェイスに絡むけれど、 Responseは単純に#finishでRackに沿った配列を返すための便利クラス。なくてもいい。

spawn-fcgi

# spawn-fcgi -U http -s /var/run/fcgi-testapp.sock /var/www/testapp/testapp.rb -n

試してるうちは-nつきにしてフォアグラウンドで実行するのが楽

実用的にする

起動スクリプト

forkingなので停止・再起動の制御のためPIDファイルを作る。

Systemd Unit

[Unit]
Description = FastCGI Rack Test Application
After = nginx.service

[Service]
Type = forking
PIDFile = /var/run/fcgi-testapp.pid
ExecStart = /usr/local/sbin/fcgi-testapp.bash
ExecStop = kill $MAINPID

[Install]
WantedBy = multi-user.target

forkingなので$MAINPIDがそのままでは使えないため、PIDFileで指定しておく。 Nginxのあとに起動しておいたほうがいいような気がしたけど、なくても構わない。 アクセスが激しい場合は逆にNginxの前に起動したほうがいいだろう

spawn-fcgi自体にはアプリをリロード、再起動するような機能はない。

おまけ

S-NailがSubjectも本文も、UTF-8をちゃんとエンコードしてくれるのですごくびっくりした。

「mailxとは違うのだよ!!!」ってことか。 さすがSMTPやPOPやIMAPにも対応しているだけのことはある。

ここの部分(MIMEエンコーディング)も自分でやるつもりだったので、かなり省力化された形。

今回の構築は他にも色々やったのだけれど、共有して意味のある部分はこれくらいのものだろう。

Mimir Yokohamaに用語集機能

Mimir Yokohamaに用語集機能が追加された。

用語集機能はACCS(PureBuilderに取り込まれる以前の)やAki PHP Content Collection時代に実装されていた。

シンプルにACCSではJavaScript+Ruby/CGIで、本文エレメント(テキストノード)を探索し、文字列を送信してRubyで置き変えたものを返してもらい、innerHTMLを置き換える方式だった。

APCCでは単純に力技で置換えていた。

PureBuilderではこの機能を実現していなかった。 正確には一時期実装していたこともあるが、十分な質でなかったのだ。

そしてついに復活した用語集機能。

Pre Pluginsで処理することも考えたが、Post PluginsでRubyで処理している。

「辞書をワード長が長い順に並べ替えて|で結合し、正規表現としてコンパイルして置き換える」という手法はかつてのコードで考えたものである。

用語集のページも単純な手法でMarkdownを生成している。

で、今回もライブラリなしのPureJSで処理している。 今回は44行ほどだが、18行は共有化できるものなので、もう少し短くすることも可能だ。 DHTMLのお手本のような初歩的なものである。

Event.stopPropagation()Event.stopPropagationと書いて悩むはめになったけど。

PureBuilder Simply 1.4 リリース

PureBuilder Simplyの1.4をリリースした

特に大きな変更点は以下の通りだ。

  • HTML生成前に処理できるPre Pluginsに対応した
  • Pre Plugins/Post Pluginsで環境変数から文書メタデータにアクセスできるようになった

Pre PluginsはPandocにかける前のドキュメントを加工するものである。 Markdownと比べReSTは自由度が低いこと、それぞれのドキュメントフォーマットに基づいて処理しなければならないことから、新たに_docformatというメタ値が追加された。

今回のポイントはPre Pluginsであり、メタデータを渡す仕様は反映していなかっただけで、実は1.2時代からあった。

Pre Pluginsはおもしろいこと書いていないので、どちらかといえばメタデータ渡しの話をしよう。

これはPre Plugionsの一部である。 IO.popenはコマンド群の前に環境変数を置くことができる。 シェル的にいうと

みたいなことだ。

もちろん、同じような手法はRubyでも使えるけれど、ちょっとめんどくさい。IO.popenの利便性は簡便に損なわれてしまう。

かゆいところに手の続くRubyは、ちゃんとそのプロセス用に環境変数を渡す方法を用意してくれているわけだ。

予め環境変数にセットするのと何が違うのか。

まず、自身の環境変数としてセットすると、メモリーを2個分使う。

また、環境変数がそのプログラム自身の制御に影響するケースでは問題が生じる。

さらに、何度もプロセスを起動するたびにセットしてしまうと、ガベージコレクションの問題が出る可能性がある。

結局、子プロセスに対して伝播したいだけの環境変数はこのプロセスに対してのみセットするのが適切、ということになる。 シェルにおいても

ではなく、

あるいは

とすべきである。

静的ウェブページでタグ機能を提供する

Mimir Yokohamaのページでタグ機能がバージョンアップし、完全に動作するようになった。

もともとWordPressで提供していたMimir Yokohamaのウェブページだが、独自システムに移行する際には「WordPressで利用していた機能はすべて提供する」という方針のもと、新しいサイト構築システムPureBuilder Simplyを開発して構築した。

PureBuilder Simplyは静的ページを生成するプログラムであり、webサーバーには静的ファイルを配置する。 これはパフォーマンス、セキュリティ、管理、リソースいずれにおいてもメリットが大きい。

基本的にこの考え方は「異なる内容を生成するタイミングより、同一の内容を返すタイミングのほうがずっと多い」ということに基づいており、キャッシュよりも合理的である。 一方、どうしても難しい要素もある。ひとつはページ生成パターンが無限である検索機能、そしてもうひとつはヒントを日本語のみにした場合のタグ機能だ。

検索機能はGoogleに頼っているが、タグ機能は難関だった。

タグ機能を作る

方針

  • 要求タイミングでの動的生成は行わない
  • PureBuilder Simplyの枠内で解決する。 もし解決不能な場合はPureBuilder Simplyを拡張する
  • (PureBuilder Simplyでサポートされている) eRubyテンプレートは使わない。あくまでPandocテンプレートで生成する
  • ファイル名が日本語になることはやむを得ないものとする (Nginxは日本語ファイル名に対してURIエンコーディングされたパスでアクセスできる)
  • リンクを日本語で書く(エンコードをブラウザに委ねる)ことは許容しない
  • ユーザー (この場合自分だけど) にタグに関して文書にタグ付けする以上の手間をかけさせない

タグをつける

既にPureBuilder Simplyでは 「Frontmatterに文書に関する追加的情報を書く」 という仕様となっている。1

単純にこれを反映したタグを書けば良い。

ページにタグ情報をつける

Pandocテンプレートで簡単につけることができる。

英語なら割とこれで済む話なのだけど2、日本語だと当然

みたいなHTMLが生成されてしまう。

そこで、post generate機能を利用する。 post generate機能はページを生成したあと生成ページを加工できるものだ。 基本的に第一引数として生成されたファイルパスが渡される。 このほか環境変数を通じて他の情報にもアクセスできたりするのだが、これはあまり利用を想定していない。

post generateは.post_generateディレクトリ内のファイルを順次Perlに渡す形で実行される。 Perlはshebang行を解釈するので、Perlで書かなければならないわけではない。 そして、スクリプトの出力にファイルは置き換えられる。

これは例えば

のような非常に簡単なフィルタが書けるということだ。

これを使って

のように変換してあげればタグのタイトルは日本語だがURIはエンコード済み、という形ができあがる。 もちろんスラッグへのマップを書いてもいいのだが、タグを管理するのは手間なので避けた。

タグページを作る

生成されたページの情報はindexes.rbmというファイルにRuby Mershal形式で保存される。 ここには本文は含まれないが、大概メタ情報にアクセスしたい場合と本文にアクセスしたい場合は別なので、分けている。

PureBuilder Simplyにおいて、「メタ情報を文書に書き、処理した情報はデータベースに書いておく」というのは設定上の核であると行って過言ではない。 これにより、文書のメタ情報を扱うことはPureBuilder Simplyの外で行うことができるのだ。

タグページは.tagcloud.rbというスクリプトによって生成しているが、 これはMarkdownページを生成する。つまり、「タグページ自体をPureBuilder Simplyによって生成すべきページとして生成する」のである。 タグを含むページを生成・更新した場合は再度タグページを生成し直すことになり、そのためにrefreshというスクリプトもある。この場合、文書ページではないタグのindexを処理されてしまうと困るので次のような処理になっている。

では.tagcloud.rbは、というとこちらも結構単純。

見ての通り著しい力技でデータベースを集計し、最終的にはMarkdownを出力している。

従来もほとんどこうだったのだが、ページ側でエスケープしていないという理由でエスケープしていなかったので追加した。

おわりに

もう少し難しいかと思っていたのだが、どうやらPureBuilder Simplyの設計が思っていた以上に優れていたようで、タグ機能もスムーズに実装することができた。

PureBuilder Simplyは傑作といって差し支えない出来になっている。 もともと思っていたよりもずっと優れたツールになっているのだ。

PureBuilder SimplyはPureBuilderとしては実に3作目である。 Zshの機能をフル活用したPureBuilder, Windowsでも動作可能なようRubyで書かれたPureBuilder2。ページの生成にはいずれも活用できたが、サイト構築労力が高く、安価な案件で利用するにはしんどいものがあった。 また、構築できる内容も割と画一的だったため、様々な要求に応えるのは難しかった。 Zshで書かれたPureBuilderは構築時に任意のZshスクリプトを実行できる方式だったため、なんでもできるといえばできるのだが、サイト構築がプログラミング色の強いものになっていた。これはちょっとユーザーフレンドリーではない。

PureBuilder Simplyは名前の通りずっとシンプルだが、いままでよりずっと強力になった。

小さなスクリプトを書くことは、多くのプログラマにとってはあまり馴染みのないことかもしれないが3、やろうと思えば発想さえ知れば決して難しいことではないはずだ。


  1. これはReSTでもdocutilが許容しないような規格化されていないメタ情報を書くということだ。

  2. ちなみに、Chienomiではタグはすべて英語になっている

  3. Unixに浸っている人はむしろ息をするようにのように行動するだろう。PureBuilder Simplyの考え方はこれに基づいている。

LuaTex-jaでNumber too big.エラーがようやく解消

LuaTexで日本語ドキュメントクラス(ltjsarticle, ltjarticleなど)を処理すると

! Number too big.
ltj@@jfont ->luafunction ltj@@jfont@inner 

l.53 \kanjiencoding{JY3}\selectfont

と言われてしまう、という問題が続いていた。 Manjaro Forumで質問したところ、再現するけどupstreamへ、ということだったのでLuaTeX-jaのほうにご報告させていただいたところ、チケットを切っていただいた

Manjaroではつい先日Texliveが201804にアップデートされたが、この問題はLuaTeX-jaが201806として解決してくださっている。

このため、

$ git clone 'https://scm.osdn.net/gitroot/luatex-ja/luatexja.git'
$ sudo rsync -r luatex-ja/src/ /usr/share/texmf-dist/tex/luatex/luatexja/

とすればとりあえずこの問題は解決する。

だが、今度は

! Undefined control sequence.
\lltjp_um_unmag_fsize: ...@preadjust@extract@font 
                                                  \cs_gset_eq:NN \lltjp_um_f...
l.106 \begin{document}

なんて言われてしまう。 これは、

ltjsarticle + unicode-math で をしていないときに起きるエラーのようです. 別チケットにします.(#38372)

とのことで、チケットを切っていただいた

現在のところkitagawa_testブランチになっているが、恐らく近日中に取り込まれるだろう。 なかなか長い戦いだったが、ようやく論文や教材の清書も捗るというものだ。

ちなみに、この影響でgendoc-pandoc.zshのほうはMathfontに対応した。

フォント帳アプリを作る → Linuxフォントの闇に触れる

追記分について

日本ではあまりなじみのない欧文コーディングフォント(プログラミング向けフォント, モノスペースフォント)をなんでこんな苦労してるんだろうとか思うくらいの労力を掛けてカタログ作ったので、気合いれて紹介させていただく

ぜひ見ていただきたい。

はじまりはmonospace

フォント一覧として“monospaceフォントの一覧を作りたい”と考えたのだ。

アプリ自体は全フォント一覧も作れるようにしたが、monospace限定でも出力したい。 どのような方法があるかと考えた。

調べるとspacingの値を見ればわかるということはわかったのだが、spacingMonospaceProportionalのような文字列ではなく、90, 100といった値になっている。

とりあえず90100もおおよそmonospaceだということはわかったが、90を提示するがmonospaceでないフォントもある。

だいたい90100の違いはなんなのか…もしかしてQtのmonospaceフォント一覧に関係があるのか…

と調べていくといろいろわかってきた。

100FC_MONOの定数であり、Monospaceのことであるらしい。 90FC_DUALであり、幅を2種類持っているもののようだ。

確かに等幅和文フォントは “monospace” ではない。 “dualspace” である。

では等幅和文フォントのspacing100にした場合Qtウィジットもmonospaceとして認識してくれるのだろうか?

FontConfigの一覧はいじれない?

で、どうやらfontconfigの設定(fonts.conf)はフォントを “要求されたとき” には反映されるけれど、 “リストするとき” には無視されるようだ。

例えばこんな設定ファイルを作る。

Migu 1Mのspacing100(FC_MONO)になる。

% fc-match "Migu 1M" : spacing family
Migu 1M:spacing=100

けど、リストには入らない。

% fc-list :spacing=100 : family | grep -i migu

Foogothicもちゃんと認識はされているけど

% fc-match -a Foogothic : family | head
Anonymous Pro
Anonymous Pro
Anonymous Pro
Anonymous Pro
Migu 1M
Migu 1M
Bitstream Vera Sans
Bitstream Vera Sans
Bitstream Vera Sans
Bitstream Vera Sans

リストにはない。

% fc-list | grep -i foo

90(FC_DUAL)100(FC_MONO)として認識させると、フォントに設定されている幅を無視するアプリケーションが存在するため、リストに入れてくれるわけでもないのならばデメリットしかない。

欧文フォントを和文で使いたいという場合には和文フォントを加える形にしてあげれば良いのだが

問題はQtである。

QtはMonospaceフォントとしてFC_MONOの値よりもspacingが大きいフォントを提示する。

問題は、FC_MONO100であり、FC_DUAL90であるため、FC_DUALなフォントは提示されない、ということだ。

この問題はバグとして報告されているのだが、今以て解決していない。

現状、monospaceを和文フォントにするか、適当な(使わない)欧文monospaceフォントをインストールして、その名前を利用して置き換えるくらいしかなさそうだ。

和文プロポーショナルフォントの話

実は和文フォントは一般的には「ラテン文字はプロポーショナル、全角文字は等幅」となっており、 日本語部分までプロポーショナルになっている「和文プロポーショナルフォント」というのはかなり珍しい。

このためか、ラテン部分がプロポーショナルであるにもかかわらず、spacing=90にしてしまうフォントが結構あるようだ。例えばDynafontとか。

なので、今回のスクリプトは-s 90とすればdualspaceフォントを含めてリストしてくれるけれども、必ずしも等幅フォントであるとは限らない。

今回のスクリプト

GitHubにアップした。

基本的にはFontConfigから値をひらってeRubyで埋め込んだHTMLを作るプログラムである。

プログラム自体は至ってシンプルなものだ。 どちらかといえばFontConfigに関する知識のほうが求められる。

READMEにある通り、うまく動作しない部分がある。 family名を指定しても正しく表示されないもの、fullnameがBoldなどになっているもの、 spacing=90といいながら等幅フォントではないもの1kanahaniが実態と合っていないもの、 などなど「フォントの情報って結構ガバガバだなぁ」と感じてしまう。 もちろん、フォントの情報から取っているわけで、がフォントにウソ情報を書かれたりするとスクリプト側では処理しがたい。

ウェイトの違うフォントが別ファイルになっているものと同一ファイルのもの、PlainやNormalなどRegularではない標準フォントウェイト名であるためにBoldが標準になってしまうもの、 ビットマップとスケーラブルがスタイルの違いになっているもの(例えばTelegramaはRawがビットマップ、Renderがスケーラブル)と厄介なものは色々ある。

欧文モノスペースフォント紹介

ボリュームがすごーく増えてしまったので独立させました

今回の件で気づいたこと

欧文フォントフェイスはかなり似ている

「読みやすいアルファベット」という条件がついていて、あまり標準的でない形は読みづらさにつながる。

結局、こうした条件のためにだいたいのフォントが似ていて、「ターミナル」「タイプライター」「セリフ」「サンセリフ」の4種類で分類できるような形状しか生まれてこない。

だが、微妙な違いが読みやすさにつながる部分もある。 現在人気のフォントはどれも似たようなサンセリフまたはターミナル体なのだけど、探せばちょっと変わったフォントもあるにはある。 人気のあるフォントの中ではMonacoやUbuntu Monoがちょっと変わったグリフをしている。逆にHackやDroid Sans Monoはものすごく普通。

商用フォントはいいぞ

Dark Mono, Operator, Native, Range Mono, Vivala Codeなど見比べて「おっ、いいな」と思うフォントは軒並み商用フォントになっている。

ただし、プログラマ、コーダーなど「日がな一日プログラムを書いている」人に関しては、これぞと気に入ったフォントがあれば買ってみるのも手ではないだろうか。 一日中向き合っているものだし。

欧文フォントも売られているものは高いものもある

Operatorなんて$199もする。 1書体のお値段としてはモリサワフォント並。 Pragmata Proもデスクトップ用 * レギュラー/ボールド/イタリックで€199

日本語フォントとは制作労力が雲泥の差なので、ちょっとえげつない値段だと言っていい。

Dark Monoも40ポンドとかなりお高め。

Atomはレンダリングが汚い

フォントのウェイトやサイズがばらつきやすく、autohintのようなガタツキも発生する。

うまく表示してくれるフォントを探すのにはずいぶん苦労した。

Atomでキレイに表示してくれるのはCPMonoなのだけど、CPMonoはコーディングフォントとして決して快適ではない。

日本語と混ぜるなら、agave, Fantasque, Fira Code, Luculent, Mesloあたりが快適。 私は結局Fira Code+Migu 1Mにした。

この組み合わせは非常に違和感がないが、後述のようにもう少し違和感があったほうが読みやすい。 その点、LuculentやMesloのほうが有力だった。

フォントの情報はいい加減

「漢字グリフはなくてひらがなだけなんだけど、haniは書いてあってkanaは書いてない」みたいなフォントもある。

商用フォントでも結構ザルである。 プログラムとしてはフォントの情報に基づいて処理するしかないわけで、「なんでここでそのフォントが?」と思うようなフォントが提示されたり、偽薬に提示されなかったりというのはここらへんに理由があるのだろう。

日本では紹介されていないモノスペースフォントはいろいろ

日本人としては日本語Glyphのないフォントは使えない、という感覚があるのだろう。 あまり紹介されることもない。

だが、実際にはプログラム中には日本語なんてほぼ使わないわけで、モノスペースフォントは日本語Glyphがなくてもあまり困らないし、 今はglyphがなければそれなりに処理してくれるのが普通なので別に好きに組み合わせても良いのではないだろうか。

日本で紹介されるものは人気があるというよりも無難なものが多く、実際にはもっと使ってみる価値のあるフォントは色々ある。

フォントレンダリングの微妙な差

KDEアプリケーションは割とにじみ強く太めに描画するため、キレイに見える。

Gnomeアプリケーションはもっとくっきり描画する。

どちらもFontConfigを使っているのだけど、描画ライブラリの微妙な差なのか、見え方が割と違う。 FirefoxとChromiumの描画差は気づきやすい。

そして前述のようにAtomはあまりよくない。

autohinterは諸悪の根源

以前は有効にするのが一般的だったように思うautohinterだけども、有効にするとガタガタになる。

フォントが寄る、ガタつく、崩れるなどのことがあったらautohinterを切るといいかも。

FontConfigはデフォルト設定がだいたいいい感じ

FontConfigの設定はディストリビューションによって作り込まれていることが多く、違いがあるけれども、 Arch設定だと色々いじってみても基本的にはデフォルトが良いようだ。

nobitmapと、サブピクセルレンダリングの配置くいらはいじったほうがいいけども。

日本語とアルファベットは差があるほうが見やすい

コーディング向きのglyphを持つ日本語フォント2の場合、統一感があるように調整されている。 「日本語とアルファベットの統一的デザインってなんだ」と思うかもしれないが、主にはグリフのボックスサイズと、線の太さである。

普通は「半角は全角の半分」となるように設計する。Source Code JPフォントの場合は全角は半角の1.5倍幅になっている。

だが、Linux環境ではこのような和文フォントのスペーシングがうまく適用されないことが多い。 Atomの場合和文フォントを指定すると縦には揃ってくれない。このことから、このようにメリットは少ない。

また、日本語にアルファベットを混ぜ書きするケース(スペースを持たないケース)においては、アルファベットは「検索(eyeboll-search)の対象」であることがあり、 区別しやすいほうが良いと考えられる。

このことを踏まえると、「日本語とアルファベットの統一感はなくても困らないし、むしろないほうが便利」だったりする。

おまけ: 今回のフォント画像に使ったコード

JavaScript組み込みHTMLのPandocテンプレートである。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="$lang$" xml:lang="$lang$"$if(dir)$ dir="$dir$"$endif$>
<head>
  <meta charset="utf-8" />
  <meta name="generator" content="pandoc" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
$for(author-meta)$
  <meta name="author" content="$author-meta$" />
$endfor$
$if(date-meta)$
  <meta name="dcterms.date" content="$date-meta$" />
$endif$
$if(keywords)$
  <meta name="keywords" content="$for(keywords)$$keywords$$sep$, $endfor$" />
$endif$
  <title>$if(title-prefix)$$title-prefix$ – $endif$$pagetitle$</title>
  <style type="text/css">
      code{white-space: pre-wrap;}
      span.smallcaps{font-variant: small-caps;}
      span.underline{text-decoration: underline;}
      div.column{display: inline-block; vertical-align: top; width: 50%;}
      body { padding: 50px; font-size: 25pt; }
$if(quotes)$
      q { quotes: "“" "”" "‘" "’"; }
$endif$
  </style>
$if(highlighting-css)$
  <style type="text/css">
$highlighting-css$
  </style>
$endif$
$for(css)$
  <link rel="stylesheet" href="$css$" />
$endfor$
$if(math)$
  $math$
$endif$
$for(header-includes)$
  $header-includes$
$endfor$
</head>
<body>

$for(include-before)$
$include-before$
$endfor$
$if(title)$
<header>
<h1 class="title">$title$</h1>
$if(subtitle)$
<p class="subtitle">$subtitle$</p>
$endif$
$for(author)$
<p class="author">$author$</p>
$endfor$
$if(date)$
<p class="date">$date$</p>
$endif$
<input type="text" size="20" id="FontForm" /><input type="button" value="Change" id="FS" />
</header>
$endif$
$if(toc)$
<nav id="$idprefix$TOC">
$table-of-contents$
</nav>
$endif$
<input type="text" size="40" id="FontName" style="border: none 0px; font-size: 26pt; background-color: #fff; padding: 5px; border: #39f double 3px; color: #333; margin-top: 2em; margin-bottom: 0.8em" />
$body$
$for(include-after)$
$include-after$
$endfor$
<script type="application/javascript">
  var fm = document.getElementById("FontForm")
  var nameBox = document.getElementById("FontName")
  document.getElementById("FS").addEventListener("click", function(e) {
    for (var code of document.getElementsByTagName("code")) {
      code.style.fontFamily = fm.value
      nameBox.value = fm.value
      nameBox.style.fontFamily = fm.value
    }
  }, false)
</script>
</body>
</html>

  1. Iosevkaはdualspace扱いになっている。

  2. そもそも合成でないフォントでそのようなフォントはかなり少ない。M+(VLゴシックとMiguもこのglyph)くらいではないだろうか。Source Han Code JPとOsaka等幅は専用に調整されてリリースされているものなので含めてもいいかもしれないが。

テキストをスクロール表示させる

プログラムについて

諸兄も一度はYouTubveで見たことがあるのではないだろうか。

文字がただ流れるだけの動画を。

自分のペースで読めない上に内容に乏しく、非常にイライラするものだが、 作業ウィンドウ以外で流しておくことでウィンドウフォーカスを切り替えなくて済む、というメリットもある。

特にmpvを使うと、右クリックで再生/停止ができるし、再生速度もコントロールできてまあまあ便利だ。

ただ、検索性がないのでやっぱりストレスである。

そこで、テキストリーダーに自動スクロール機能が欲しい、と思った。

ありそうだが、見当たらなかった。

もちろん、一行単位であればsleepreadでも組み合わせればいいのだが、これはパッと動いてしまうため目の追従が難しく、読みづらい。 1ピクセルずつ動かしたいのだ。

xdotoolを使う、という方法も考えたのだが、意図したようにはうまくいかなかった。

JavaScriptを使えばすごく簡単なのに…というわけで、JavaScriptを使う方向でがんばることにした。

すごーーーーく単純に一時ファイルでHTMLを作ってQtWebengineで表示すればいいと思って、Pythonで作ったのだが…

…Surfでよかった。

まぁ、配布するならSurfみたいなマイナーなものは入れたくないなんていう天邪鬼さん1や、Surfは入っていないなんていう弱小ディストリさんはいっぱいいると思うので良しとしよう。

そんなわけで作った。

珍しく日本語READMEもある。 BGM機能があるのは、YouTubeでよく見るものをリスペクトしたものであり、背景画像がほしければCSS編集でOKだ。

やっていることはごく単純で、Pandocで処理することを前提として一時ファイルを活用している。

プレーンテキストの場合はsedを使ってラインブロック化している。 この場合改行されなくなってしまうので、pre-wrapするようにデフォルトのスタイルシートで変更している。

スクロール制御はJavaScriptでものすごく入門的なコードだ。 右クリックでも再生・停止できるようにしているのは、コントロールをウィンドウフォーカスしてからでなくても行えるようにするためである。

ちなみに、今回のJavaScriptは互換性をあまり気にしない贅沢なコードでもある。 これはかなり新しいGtkWebkit、あるいはQtWebengineを使うという前提が成り立っているためだ。

解説

それでは恒例の初心者向けコード

JavaScript部分

せっかくなので、入門的なJavaScriptコードを解説しよう。

スクロール部分

まずスクロール自体はwindow.scroolByによって実現できる。

自動スクロールをするためにはこれを繰り返すようにしなくてはいけない。 もちろん、単純なループでもできるのだが、JavaScriptはシングルスレッドなので操作不能になってしまう。

このような反復はJavaScriptではタイマーイベントで行う。 タイマーにコールバック関数を登録することで、タイマー起動時にコールバック関数が実行される。 もし他の処理が実行中の場合は処理をキューに入れ、順番がきたら実行される。

反復の場合setIntervalのほうが簡単で、初心者向けの解説ではよくこちらが使われるが、 どちらかといえばタイマーイベントを都度登録するsetTimeoutのほうがコントロールしやすい。

関数オブジェクト、クロージャという考え方になれていないとそもそもコールバック関数が使えないので、ここはしっかりと理解しておく必要がある。

関数オブジェクトは実行可能なコード群をオブジェクト化したものである。 スイッチを押せば実行される物体になっているとでも思えばいい。 コールバック関数は、それを呼び出すタイミングでそのスイッチを押すように動作する。

setTimeout は指定した時間が経過したときにコールバックを行う。 コールバック関数の中で自身をコールバック関数とする setTimeout を呼ぶことでループさせることができる。

「スクロールの開始」はその関数を呼べば良い。 setTimeout で呼ばなくても一度呼べば setTimeout によってループする。

「スクロールの停止」は setTimeout をしなければ次回の実行がなされないため、停止する。 停止方法はタイマーをキャンセルするのではなく、次回のタイマーセットを行わないというものである。 このため、タイマー動作状態がonの場合のみ setTimeout を行う。

setInterval を使用した場合はタイマーをセット/キャンセルして制御する。

キーイベント部分

キーボードのキー入力は document あるいは window に対してイベントリスナーを設定する。

DOM Level1 のonKeydown を設定する場合の情報は割とあるのだが、 DOM Level3 の addEventListener を設定する情報はやや少ない。

コールバック関数の引数としては KeyboardEvent オブジェクトが渡される。 特定のキーをキャプチャするわけではなく、キーボード入力全てをキャプチャしてコールバック関数が呼び出される。 コールバック関数内でキーを判別する。

なお、コールバック関数が時間がかかるとフリーズしていると感じることになるため注意が必要。

キーの種別を取るための方法は色々あるのだが、 code が推奨される。 プリンタブルなキーに限定するのであれば key でも良い。 charcharCode はあまりうまく動作しないし、 keyCodewhich は廃止されている上に、プラットフォームに依存する。

それぞれどのような値になるのかは次のようなコードを書いて確認すれば良い。

元のキーの動作を無効にする場合は、第三引数を false として先にキーを捕捉してから Event.preventDefault によってバブリングを停止する。

右クリック禁止2、でお馴染み右クリックは contextmenu イベントになる3

スクロールスピード

スクロールは

  • スクロールする時間間隔が短くなると速くなる
  • 一度にスクロールする量が増えると速くなる

量が増えるとスムーズさが書けるため、時間を短くするほうが優先。 間隔が1(1ミリ秒)になった場合、加速はスクロール量によって行う。

逆に遅くするときはスクロール量が増えているならそちらを先に減らす。スクロール量が1であれば時間間隔を増やす。

1ミリ秒刻みでは使いにくかろうと思ったので、5ミリ秒刻み、ただし値が小さいほうが変化量は大きいため、5ミリ秒からは1ミリ秒で調整されるようにしている。 (50ミリ秒から+5された場合は10%遅くなるが、10ミリ秒から-5された場合は100%速くなる)

割合で増減させてもいいのだが、どちらかといえばキーリピートが効くため一定間隔にしたほうが良いUIであると考えられる。

今回は50ミリ秒をデフォルトとしたため、10%の5ミリ秒を増減単位とした。 なお、この速度感は画面のピクセル数によって異なり、特にピクセル数が多く高精細なディスプレイの場合は遅く、ディスプレイサイズが大きくピクセル数が少ない場合は速く感じることになる。

設計

基本的には「得意なことは得意な方法で」だ。

このようにスクロールやキーイベントなどはウェブブラウザとJavaScriptが簡単に書ける。 だから無理せずウェブブラウザとJavaScriptで書こうと考えたわけだ。

ただし余計な機能や情報があると使いにくい。 最低限のウェブブラウザが欲しいのだが、既にそのようなものはSurfがあるのでこれを使う。

もっとも、レンダリング部分はQtwebengineやGtkWebkitがあるのだから書くのは非常に簡単である。

もちろん、そのためにはテキストをHTMLにする必要がある。

テキストを正確にHTMLで表現するのはちょっと大変だが、CSSで white-space: pre-wrap にしてしまえばタグ要素と & をエスケープしてしまえば元のテキストを表現できる。 このようなことはSedでもできる。 ただし、 & を先にエスケープしなくてはならない。エスケープがエスケープされることを防ぐためだ。

テキスト処理するためのツールはLinux上に豊富にある。このようなことはシェルが得意とする部分だ。 文字列を埋め込むだけならば特別なツール(例えばeRuby)を使わなくても、ヒアドキュメントで十分だろう。 プログラムはシェルスクリプトで構築する、という方針は簡単に決められる。

中間的なファイルや生成に必要なファイルは、もちろん予め用意してリンクさせることもできるが、ディレクトリ設計に関する障害を増やすことになる。 今回はローカルディレクトリにインストールする、という前提を与えてはいるが、それでもできれば避けたいところだ。 それにバージョンアップの手間も考えれば、一時ファイルを作る方針にした。 これも簡単なものなのでヒアドキュメントで処理する。

ブラウザを作るにあたっては、私の得意なRubyではなく、割と苦手なPythonにした。 Rubyにもqml Rubyバインディングは存在するし、動作もするが、メンテナンスされておらず(3年間放置されている)、情報も非常に少ない。 また、ruby-qmlよりもpyqtのほうがqml自体もシンプルに書けるので、Pythonを採用した。

表示はウェブブラウザ(作ったもの的にはPython+qml+Webengine)で、スクロールはJavaScriptで、変換と橋渡しはシェルスクリプトで。 コンポーネントを分けて、それぞれが得意なことをシンプルに行う。 問題を簡単にし、ミスを減らすポイントでもある。


  1. もっとも、Unsurfを動かすためにはPyQt5とpython-openglが必要なので、ハードルの高さはどちらが上やら

  2. だいぶ懐かしい響きだが、今でもしているところはある。あまり意味はない。

  3. より正確にいえば、これはコンテキストメニューを表示させたときに発生するイベントで、右クリックと一対一ではない。キーボードのメニューキーを押した場合や、右クリック以外をコンテキストメニューキーにしている場合も同様にも発生するし、右クリックがコンテキストメニューでないのならば発生しない。