Hatena::ブログ(Diary)

Alone Like a Rhinoceros Horn

2012-02-21

unite-outline の近況: 次期バージョンの開発

昨年の秋頃から目立った変化のない unite-outline ですが、開発が停滞しているとか、作者のやる気がなくなった*1とかではなく、「今年の抱負 - Vimプラギン編」で述べた通り、次期バージョンの開発(準備)を少しずつ進めています。

ある程度形になったら別ブランチを切って push しようと思ってますが、まだ仕様をあれこれ考えたり非同期実行のやり方を模索している段階で、ものは全然できておりません(汗 まあ、上半期のうちに最初のバージョンを push できたらいいな、ぐらいの見込みです。*2

大きな変更が必要になるついでに、「もうこの際、たまった負債を全部返済してやる!」みたいな意気込みでして、プラグインの構成上前から気になっていた点や、作者がまずいと思っている outline info の仕様など、全面的に見直してドカンと刷新する予定です。(というか、そうせざるを得ないのです…… 詳細は以下を参照)

で、その次期バージョンですが、青写真は以下の通りです。

NuOutline(仮)

アウトライン解析部を独立したプラグイン(NuOutline(仮))として切り出し、unite-outline をそのフロントエンドとして添付するという構成をとります。

プラグインの構成上一番大きな変更点がこれです。

「unite.vim の source として unite-outline は大き過ぎる」、「アウトライン解析を行うプラグインに unite-outline を添付すべきでは?*3」という指摘は以前から何度かあり、自分でも気になっていました。また、

  • autoload/unite/sources/outline/ 以下のディレクトリ階層の深い位置にファイルがごちゃごちゃと多過ぎる。
  • その結果、オートロード関数の名前がやたら長くなる。
  • また、動作が複雑になってオプション変数も増えたが、unite.vim の source のそれはやはり名前が長くなる。
  • unite.vim の source とアウトライン解析部が渾然一体、なんだかなー

といったこともあり、アウトライン解析を行う部分を独立したプラグインとして切り出し、プラグイン全体の構成を仕切り直すことにしました。図で示すと以下のようになります。

f:id:h1mesuke:20120221221020p:image

きれいに 3層に分離しました。3者の役割分担は Webアプリケーションなどでおなじみの MVCパターンでとらえると理解しやすいです。アウトライン表示のインターフェースを提供してくれる unite.vim がビュー、見出し抽出のロジックを持っている NuOutline がモデル、両者の間に入ってあれこれと調停をし、見出しの自動更新のための面倒をみたりする unite-outline がコントローラというわけですね。

V, C は交換可能(にしたい)

NuOutline は(頑張って)unite.vim, unite-outline と密に結合させないで作ろうと思っています。unite-outline はあくまでフロントエンドのデフォルト実装として NuOutline に添付されているもの、という位置付けにして、第三者が別のフロントエンドを提供できる余地を残しておきます。(そんな人が現れるかは別にして ;-) )

が、リポジトリは unite-outline のままで!

どちらかというと unite-outline の方が NuOutline のオマケ的な存在なのですが、Vundle や neobundle.vim 全盛(?)の昨今、リポジトリURL が変わるというのは致命的だと思うので、開発は引き続き、現行の unite-outline のリポジトリで行います。まあ、使うだけなら NuOutline は unite-outline の影に隠れて見えませんし、ユーザーが使うのはあくまで unite.vim を通してですからね。

というわけでなんと、unite-outline という unite.vim の source に、プラグインがまるまる1個付いてくるということになります(笑)

オプション変数名が変化

「unite.vim上でどう表示するか」に関する設定を除き、オプション変数の所管が NuOutline の方へ移るので変数の名前が変わってしまいます。しばらくは古い変数の方も見るようにしたいところですが……

outline info → parser

unite.vim の source である unite-outline の source……的な存在であった outline info は所管が NuOutline の方へ移ります。それにともない名称も outline info から parser に変更し、配置場所も

  • autoload/nuoutline/parsers/

とします。*4unite.vim における sources, kinds, filters などと同様の位置付けになります。

DRY でない create_heading() よ消え去れ!

outline info の仕様には(今となっては)気に入らない部分がたくさんあり、その最たるものが見出し抽出のためのパターンが複雑になると create_heading() の中身が DRY でなくなるというものです。例えば、

'^\s*\(foo\|bar\|baz\|fizz\|buzz\)\>'

のようなパターンを設定したとして、マッチングが行われ、マッチが見つかって create_heading() が呼び出されたとします。その場合、マッチしたサブマッチがどれかによって処理を分けなければならないことがほとんどで、現行の outline info ではマッチした文字列に対して再度マッチングを試み、サブマッチのうちのどれに実際にマッチしたのかを判定するような DRY でないコードが氾濫しています。

これは outline info を書く側の問題ではなくて、そのようにしか書けなくしている outline info の仕様の方に問題があるのです。

「こんなことでは outline info を書いてみようと思ってくれた人も離れていくわ!」

と強く思った私は、outline info の所管が NuOutline へ移るこのタイミングで、その仕様を刷新することにしました。

まだ仕様を練っている段階ですが、

let s:parser.foo_pattern = "foo's pattern"
function! s:parser.create_foo_heading()
endfunction

let s:parser.bar_pattern = "bar's pattern"
function! s:parser.create_bar_heading()
endfunction

let s:parser.baz_pattern = "baz's pattern"
function! s:parser.create_baz_heading()
endfunction

こんな風に、見出しの種類ごとに個別にパターンを設定できるようにし、「どのパターンにマッチしたか」の判定までを本体側の責任として、適切なコールバックが呼び出されるようにします。parser側では見出し(辞書)の作成に専念できるようになるので、パターンマッチ方式の parser を作るための敷居はかなり下がるはずです。

構文解析が必要になるケースのサポート

create_heading() について力説した直後にアレですが、「正規表現によるパターンマッチ → create_heading() 」というやり方で見出しが拾えるファイルタイプは実は少なく、ほとんどのプログラミング言語では見出しを「ちゃんと」拾うのに構文解析が必要になります。

具体的に言うと、ctags などの外部の構文解析プログラムを呼び出し、その出力を解析して見出しの木を parser が自前で作る必要があります。

今後、対応ファイルタイプを増やしていくためにも、その辺の処理が書きやすくなるようなユーティリティコードの充実やドキュメントの整備などが必要と考えています。

まとめ

最初からちゃんと設計しとけや! ヽ(*`Д´)ノゴルァ

うう、当初は「正規表現でマッチさせて拾うだけやん」程度の軽いノリで作り始めたのでございます。最初にコミットされたバージョンの素朴さがすべてを物語っている……

しかし、まあ、ここまできたらプラグインとしてちゃんとしたものにしたいですね!

*1:むしろ逆で、変なブーストが入ったw

*2:今年の抱負ということになってるので、遅くとも年内にはw

*3:alignta に unite-alignta が同梱されているのがこの構成ですね。

*4:outline info は autoload/outline/ に配置してもよいことになっていましたが、これは autoload/unite/sources/outline/ が自作の outline info を配置する場所として位置が深すぎるだろうとの配慮からでした。通常、autoload/ に何かを配置させるにあたっては、プラグイン名できちんと名前空間を切るべきです。

2010-01-27

Greasemonkeyスクリプトの開発で役に立ったサイトや本

Firefox にこんな機能があればいいなあ → ん、Greasemonkey というのでできるらしいぞ → ユーザースクリプトとやらを書けばいいのか → どうやって書くんだ?

というところからスタートして、最終的に自作のユーザースクリプトを公開するに至るまでの間、参考にしたサイトや本をできるだけ自分の学習順に時系列に沿って列挙してみました。

JavaScript を少々かじったことのある人が Greasemonkeyスクリプトを書いてみようと思い立ったときに、その学習の指針というか、道標のようなものとして役立つリンク集になればいいなと思ってます。

Greasemonkey

まずは Greasemonkey ってなんだとか、ユーザースクリプトってどう書くんだというのを調べるところからスタート。(以下小見出しがリンクになっています)

Greasemonkeyの開発をまとめてみる

screenshot

ここで Greasemonkey とは何をするアドオンで、そのユーザースクリプトはどう書くのか、その体裁の大まかなところを掴みます。「// ==UserScript==」で始まるスクリプト冒頭のメタデータブロックの書き方と、GM_log() で Firefox のエラーコンソールにデバッグのためのログを出力できることぐらいを押さえておけば、後はページの DOM操作、ということになります。

ただ、この先さらに調べていくとわかりますが、

無名関数でラップかけて、コードを書かないと名前の競合がおこってしまう。

の部分はかなり古いバージョンの Greasemonkey での話なので、今となってはやる必要はないと思います。

追記:ユーザースクリプトFirefox + Greasemonkey という環境で実行されるとは限らないため(Opera とか)、無名関数でコードを囲っておくというテクニックは今でも有効で意味があります。以下のエントリと、そこから参照されているエントリが大変参考になりますので、一読をお勧めします。

Dive Into Greasemonkey

screenshot

で、実際に何か書いてみようとなる訳ですが、その前に目を通しておいた方がいいのが「Dive Into Greasemonkey」です。これは Greasemonkeyスクリプト開発の定本と言ってもいいドキュメントで、

など、開発の基本がコンパクトにまとまっています。

中でも「4. Common Patterns」は DOM の扱いに慣れていなかった自分には非常に有用で、スクリプトを書きながら何度も参照しました。

mozdev.org - greasemonkey: authoring

screenshot

「Dive Into Greasemonkey」を補足するドキュメントです。unsafeWindow ってなんじゃらほい? と疑問に思ったらこのドキュメントの該当箇所を熟読です。安易な unsafeWindow の利用がなぜ、どのように危険かといったことが説明されています。これで unsafe の意味がわかれば、知らず知らずとっても危ないスクリプトを書いていた、なんてことはなくなると思います。

Greasemonkey Manual:API - GreaseSpot

screenshot

「Dive Into Greasemonkey」が入門編・チュートリアルであるとすると、こちらは Greasemonkey の仕様/API を網羅的に解説したリファレンスです。

「Dive Into Greasemonkey」の APIリファレンスでは一部のメソッドが紹介されているだけですが、こちらには完全な APIリファレンスがあります。また、メタデータブロックの仕様も詳細に解説されています。

Greasemonkeyの共通な落とし穴を避ける - minghaiの日記

screenshot

スクリプトが思った通りに動かない。そんな落とし穴にハマったらここを見てみます。簡単にまとめると、

あなたが Element と思っているそれ、それは実は Element ではなくて、Element をラップした XPCNativeWrapper なんだYO!!

ということに尽きると思います。

JavaScript

JavaScript そのものについても、コアライブラリや DOMリファレンスをいつでも参照できるようにしておいた方がいいでしょう。

JavaScript Reference - MDC

JavaScriptリファレンス」という名前のサイトは Web上にたくさんあるのですが、Greasemonkeyスクリプトの実行環境はあくまで Firefox なので、mozilla.org のそれが公式といえば公式でしょう。

全体的にレスポンスがもっさりしています。後、日本語版ページからだとリンクがうまく辿れないことがあって、そんな場合は検索結果から英語ページに飛ぶか、最初から英語版を参照した方が無難です。

DOMリファレンス

英語版のホーム

JavaScript style guide - MDC

Greasemonkey とは直接関係ないですが、コーディングスタイルで迷ったらここを見てみます。あくまで参考程度に。*1

JavaScript 第5版

JavaScript 第5版

サイ本。オブジェクト指向風味で書いてみるか! とか思ったりした時に手元にあると安心です。やみくもに prototype とかいじる前に読むことを推奨。(もちろん、その辺を解説した他の本でもサイトでもいいです*2

XPath

DOM要素の切り出し(選択)に XPath は必須の知識と思われます。Greasemonkeyスクリプトでも、DOM要素の切り出し用に document.evaluate() をラップした getElementsByXPath() なんて名前の関数がよく定義されて使われています。

そんな訳で、XPath の書き方について調べます。(知ってる人は飛ばして下さい><)

XPath 教程

screenshot

「Dive Into Greasemonkey」よりのご紹介。サイト名に漢字が使われていますが海外のサイトです。Example を順番に見ていけば XPath の書き方が大体わかります。

XPathXML -TECHSCORE-

screenshot

XPath はそれなりに使えるようになったけど、なんだかもやもや、軸とかノードテストとか言われてもピンとこない…… という場合はここでみっちり勉強しましょう。基礎からきちんと解説してくれる上に、図解が多用されていて非常にわかりやすいです。ここを読み終わる頃には、

id("PAGEBODY")//table[@class="pager"]/preceding-sibling::*[not(./descendant-or-self::form[@name="sort"])]

こんな XPath を理解し、また書けるようになるでしょう。

特定のclass属性を持った任意の要素にマッチするXPath | 3.14

screenshot

//div[@class="something"]

なんてうっかり書いていませんか? class属性は複数の値を持てる上、その区切りには改行やタブが使われることもあるよ、という内容。自分で class属性を設定した要素を選択するだけならともかく、そうでない場合は XPath の書き方に配慮が必要です。

特に、実行時に動的に要素のスタイルを切り替えるようなページの場合、ソースの見かけでは「class="something"」となっていても、ロード後にページの JavaScript によって class属性が追加されて実質「class="something even"」のようなことになっているかも知れません。このような場合、上記の XPath ではマッチに失敗してしまいます。

class属性に "something" を含む、という意味の XPath にするには、

//div[contains(concat(" ", normalize-space(@class), " "), " something ")]

とする必要があります。

GreasemonkeyスクリプトのGM_xmlhttpRequestとかで取得したhtmlとかxmlをいじる場合はxpathだと遅いことが多いかも - さらさら宇宙忍法帖

screenshot

GM_xmlhttpRequest() で取得した HTMLXML をパースする場合、XPath を使うより正規表現を使った方がかなり高速です。というのも、GM_xmlhttpRequest() はレスポンスから DOMツリーを構築してくれないからで、XPath を適用するためには自分でレスポンステキストから DOMツリーを構築しなければならず、そのコストが高くつくからです。

実際に速度比較を行ってみたところ、正規表現の方が概ね 10倍以上高速でした。(もっとも、10倍といってもミリ秒の世界のことなので、その違いを体感できるかは微妙ですが)

XPath を使うためにやたらゴニョゴニョやってるなーと思ったら、そんなものはバサッと削って正規表現を使う、という選択肢もあっていいと思います。

設定画面

スクリプトを作り込んでいくと、設定によって動作をあれこれ変更できるようなものへと進化していくことが多いと思います。そんな時、「設定を変更するにはスクリプトを編集して下さい」というのはちょっと格好が悪いです。*3そこで、Greasemonkeyスクリプトに設定画面を追加してくれるライブラリを探すことになります。

GM_config - a configuration GUI for your scripts – Userscripts.org

screenshot

メニューから呼び出せる設定用GUI を比較的簡単に Greasemonkeyスクリプトに追加できるライブラリとして GM_config があります。

使い方はそんなに難しくなく、上記の API reference や GM_config を使っている既存のスクリプトなどを参考にすれば大体のところはわかると思います。ただ、

  • 設定画面のレイアウトを思った通りにやりにくい
  • 設定画面がたまに出ない*4

といった問題があって、そういう点に不満がある場合は自分でコードをいじってどうにかするぐらいの意気込みで使うべきものだと思います。*5

なお、こういったライブラリを利用したスクリプトを配布する場合、レポジトリの trunk から @require するのは、よほど信頼のおけるプロジェクトでない限りやめた方がいいと思います。安定動作を確認したバージョンを自分の管理下にあるサーバーに配置し、そっちを @require するようにした方がなにかと安全です。*6

Greasemonkeyスクリプトに設定画面を追加するライブラリ USConfig

手前味噌になりますが…… GM_config に対する不満点を解消すべく私が開発しているライブラリです。一応紹介(;^ω^)開発終了しました。

自動更新機能

スクリプトをリリースしてからも機能を追加したりバグを直したりと、スクリプトのさらなる改善は続きます。そんな時に問題となるのが「自分がスクリプトアップデートしたことをどうやってユーザーに告知するか」です。

スクリプトの自動更新機能を提供するものとしては以下のものがあります。

Wescript

ユーザースクリプトの自動更新機能を提供するアドオンです。スクリプトの配布元ページでこのアドオンのインストールをお願いするのが一番楽な方法です。

screenshot

We Ain’t Seen Nothin’ Yet. : Greasemonkeyスクリプトに自動更新チェック機能をつける方法 改

screenshot

こちらはスクリプト自身に自動更新機能を持たせるというアプローチです。エントリ内で配布されているサンプルスクリプトからコードをコピペして使うというスタイルです。

しかし、肝心のコピペ部分にミスってる部分があったりするので使う場合は修正が必要です。

--- update-notification-fu.user.js  2010-01-26 23:04:52.000000000 +0900
+++ update-notification-fu-fix.user.js  2010-01-26 23:04:48.000000000 +0900
@@ -50,7 +50,7 @@
                 '\u73FE\u5728\u304A\u4F7F\u3044\u306E',
                 'Greasemonkey\u30B9\u30AF\u30EA\u30D7\u30C8 \u0027',
                 this.script_name,
-                '(var. ', this.current_version, ')',
+                '(ver. ', this.current_version, ')',
                 '\u0027 \u306F\u65B0\u3057\u3044\u30D0\u30FC\u30B8\u30E7\u30F3 ',
                 this.remote_version,
                 ' \u304C\u516C\u958B\u3055\u308C\u3066\u3044\u307E\u3059\uFF0E',
@@ -129,7 +129,7 @@

         // Check script update remote
         check_update: function() {
-            if(!this.has_need_for_check) return;
+            if(!this.has_need_for_check()) return;
             var user_script = this;
             GM_xmlhttpRequest({
                 method: 'GET',

AutoPagerize

AutoPagerize はおそらく最も利用されているユーザースクリプトです。Greasemonkey のあるところ AutoPagerize あり、と言っても過言ではないと思います。なので、自分の書いたスクリプトAutoPagerize と併用される可能性は(どこを @include するかにもよりますが)必然的にかなり高くなります。AutoPagerize が利用可能なページで動作するスクリプトAutoPagerize と連動できないのは致命的で、AutoPagerize を愛用しているユーザーには使ってもらえない可能性が大です。

というわけで、AutoPagerize に対応したスクリプトにしようということになります。

AutoPagerize Wiki: APIs (ja)

screenshot

ほとんどの場合、このページにある Sample をそのまま流用して、pop_alt() を自分のスクリプト関数で置き換えることで対応できると思います。*7

  if (window.AutoPagerize) {
    boot();
  } else {
    window.addEventListener('GM_AutoPagerizeLoaded', boot, false);
  }
  function boot() {
    window.AutoPagerize.addFilter(function(pageElems){
      pageElems.forEach(yourFunc);
    });
  }

↑大体こういう形になります。この例の場合だと、AutoPagerize が次のページをロードする度に、AutoPagerize が切り出した次のページの pageElement を引数として yourFunc() が呼び出されることになります。

ここで注意すべきは、AutoPagerize によってロードされたページが単独の pageElement であるか、それとも複数の pageElement からなるかは pageElement の切り出し用 XPathの 定義(SITEINFO)による、ということです。

ユーザースクリプトでは DOM を操作する訳ですから、pageElement として何が渡されてくるかは把握しておく必要があります↓

アイテム - データベース: AutoPagerize - wedata

screenshot

AutoPagerize は次のページをロードするための nextLink や pageElement の切り出し用 XPath の定義(SITEINFO)をこの外部 DB に持っています。ここであなたのスクリプトが @include しているサイトの pageElement 切り出し用 XPath の定義を調べ、何が pageElement として渡されることになるかを確認しましょう。

例えば、Google の検索結果ページであれば、pageElement は以下の XPath で切り出された要素になります。

id("res")/div[ol or div]|id("ofr")

最後に

自分自身まだ Greasemonkeyスクリプトを書き始めて日が浅いので、手持ちのリソース(というかブックマーク)はとりあえずこれぐらいです。今後も開発に役立つサイトやページを見つけ次第、随時追加していく予定です。

*1:ちなみに自分のスクリプトは逸脱しまくりですw

*2:と言いながら自分が持ってるのは今となっては古い第3版だったりするのであまり偉そうなことは言えません(汗)

*3:自分が最初にスクリプトを公開した時はまさにこれで、その後 GM_config の存在を知り大急ぎで設定画面を追加したという恥ずかしい経緯があります。

*4:これは GM_config の問題というか Firefoxバグではないかと睨んでいるんですが詳細はわかりません。現在調査中。

*5:かくいう自分もコードをいじって機能追加などを行っているうちに完全に既存のものと非互換のものになってしまい、パッチを送るに送れなくなったという経緯があったりします。

*6:実際、GM_config に対し、設定のデフォルト値を指定するキーである default を _def に変更するコミットがされたことがあり、それが原因で GM_config を利用するスクリプトが動かなくなったことがあります。

*7:ただ、個々のスクリプトにこういった対応を強いている現状は正直どうかなと思う。できれば AutoPagerize にはもうひとつメタなレベルで動いてもらって、ページのロードに際して Greasemonkey 自体を起動する側にまわってもらえると有難いかも。多分アドオンになる必要があるけど。

2009-04-13

svnserve: command not found への対処@さくらインターネット

さくらインターネットのレンタル鯖で個人的に管理している Subversion のレポジトリに突然アクセスできなくなった。昨日まで普通にコミットできていたのに、今日朝一でコミットすると

bash: svnserve: command not found
svn: Connection closed unexpectedly

と出た。

色々調べてみるとどうもこの辺が原因らしい↓
対処法も紹介されている。

なるほど。エラーメッセージの通り、svnserve が見つからないわけだ。
$HOME/local/bin なんてとこに入れてるしな。

でもなんで昨日までは問題なかったんだろうと不思議に思って鯖にログインしてみると、

FreeBSD のバージョンが上がっていた

ああそういうことか。OS のバージョンうpにともなって sshd の設定が変わってしまったんだな。そういや、OS のバージョン上げるためのメンテやるぞって告知のメールが来てた。どうやらそのメンテが今朝行われたようだ。

というわけで、シンボリックリンクで対処。
svnserve のある $HOME/local/bin を $HOME/bin で参照できるようにすると……

Transmitting file data ......
Committed revision 626.

無事にコミットすることができた。

2007-03-23

本棚:開発環境

エディタ

Vim
Vimテクニックバイブル ?作業効率をカイゼンする150の技

Vimテクニックバイブル ?作業効率をカイゼンする150の技


ViIMproved‐Vim完全バイブル

ViIMproved‐Vim完全バイブル

vim の機能を網羅的に解説。
訳がところどころ変なのと、情報が分散しているのが難点。手元にあると便利だけど、Vim script を書こうとしたときには正直あまり役に立たなかった。


入門vi 第6版

入門vi 第6版

Emacs

Web版


Web版


Emacs Lispプログラミング入門

Emacs Lispプログラミング入門

Web版


Meadow/Emacsスーパーチュートリアル (Front Programmer Series)

Meadow/Emacsスーパーチュートリアル (Front Programmer Series)

シェル

VCS

Git
Pro Git

Pro Git

実際に読んだのはこの原著ではなくて、Web上にある日本語訳のほう。

図がふんだんに使われており、コミットすると何が起こるか、ブランチとは何か、といったことが視覚的に一発で把握でき、非常にわかりやすい。Git の入門書として良書。実際、これを読んだ後、自分の個人用 Subversionリポジトリをすんなり Git に移行できた。

Subversion
Subversion実践入門?達人プログラマに学ぶバージョン管理

Subversion実践入門?達人プログラマに学ぶバージョン管理

第2版が既に出ている↓

 Subversion実践入門:達人プログラマに学ぶバージョン管理(第2版)