daily dayflower

2009-01-19

JavaScript でかんたん XUL アプリに挑戦

こんにちは!

みなさんガジェットつくってますか!

なんだか最近、色々な種類があるみたいですね!

(以下略!)

そしたら意外と簡単だった…!

これならぼくにも作れそう!!

ってことで、ちょっとメモしておきますね!

どれにしようかな…!

  • おもしろいこと
  • Ubuntu で動けばいいや*1
  • 自分とこで書きやすいのがいい

こんな感じで考えていくと…

最終的に XUL アプリケーション,ってことになりました!

XUL で作られたツールなら,いつも使っているよ!

Firefox とか Thunderbird とかね)

だから XUL アプリに決定!

つくるのむつかしそう?

XULアプリって C から libxul をゴリゴリ触らないとダメなんじゃないの?

なんて思っていた時期がぼくにもありました…!

大丈夫!XULRunner を使えば(HTML に似た)XULJavaScript だけでできるよ!

だからホームページ作るのと同じくらい簡単に感じる人もいるかもしれないですね!

ブラウザ上のページだと、JavaScriptとかで他のサイトの情報を取得できなかったり

ファイルの読み書きできなかったりと、色々な制約があるんだけど、

XULRunner なら XPCOM もあるから

そんな制約なしで、ウイルスでも何でも好きなものがバンバンつくれます!

やったね!

つくるための準備

準備は Ubuntu Linux 8.10 でのやりかたです!

Windows とか Mac とか一般的な OS 使ってる人はどうせニヤニヤしながら眺めているだけだろうから,ほんとにやりたい人は no title を参考に自分で適当にやっちゃってください!

Ubuntu だととくになにもしなくても*2 XULRunner が入っていたから省略するよ!)

ちゃんと設定できてるかな?

端末を開いて,xulrunner って入れてみよう!

なんかでてきたら XULRunner はインストールされてるよ!

よし!つくろう!

ぼくはいつも ~/tmp/ にガラクタファイルを溜め込んでいってるので,

~/tmp/myapp ってフォルダを作ってみました!

はい!今回つくるやつは「myapp」っていう XUL アプリです!

作業用フォルダに必要なフォルダを用意する

シンプルな XUL アプリの場合,だいたい下記のようなフォルダ&ファイル構成になるよ!

/myapp
  /chrome
    /content
      main.xul
    chrome.manifest
  /defaults
    /preferences
      prefs.js
  application.ini

だから,まずディレクトリをきっておこう!

% cd myapp

% mkdir -p chrome/content defaults/preferences

作業用フォルダに必要なファイルを用意する

4つのファイルを用意しよう!

ひとつが,XUL アプリの情報を設定する application.ini ファイル。

もうひとつが,使用するリソース(画像とか JavaScript ファイルとか)の場所のレイアウトを指定する chrome.manifest ファイル。

さらにもうひとつが,設定を書くための prefs.js ファイル*3

最後が,メインになる main.xul ファイル!

XUL アプリの情報を設定する application.ini ファイル

作業用フォルダの直下に新しく「application.ini」ってファイルを作ってね!

% vi application.ini

中身は…

[App]
Vendor=dayflower
Name=My App
Version=1.0
BuildID=20090119
ID=xulapp@example.org

[Gecko]
MinVersion=1.9
MaxVersion=1.9.0.*

これをコピペでok!

(ほんとは Vendor とか ID を適宜書き換えてほしいけど…)

リソースレイアウトを設定する chrome.manifest ファイル

作業用フォルダの chrome/ フォルダ以下に chrome.manifest ファイルを作ろう!

% vi chrome/chrome.manifest

中身は…

content myapp file:content/

たったこれだけ!これもコピペでok!

念のためにちょっと解説?すると,XUL アプリケーションFirefox 拡張機能では,使用するファイル(画像とか CSS とか html とか xul とか js とか)を chrome/ フォルダ以下につっこむんだけど,一般的にはこの chrome/ フォルダ以下を JAR ファイル(ZIP 形式)で圧縮して配布することが多いんだ。だけど上記のように書くと,圧縮はしてなくて,chrome/content/ フォルダ以下にそのままおいてあるよ,という意味になるよ!

設定ファイル prefs.js ファイル

作業用フォルダの defaults/preferences/ フォルダ以下に prefs.js ファイルを作ろう!

% vi defaults/preferences/prefs.js

中身は…

pref("toolkit.defaultChromeURI", "chrome://myapp/content/main.xul");

pref("browser.dom.window.dump.enabled", true);

これもまたコピペでok!

念のために説明すると,上の行は XULRunner にメインウィンドウとして使われる XUL ファイルの名前を教えてあげるための設定だよ!「chrome://」という形式からもわかるように,これは chrome.manifest にさっき設定したマッピングと関連している。さきほど chromemyappcontentfile:content/*4 って設定したので,chrome://myapp/content/main.xul という表記は,(chrome/)content/main.xul というファイルを指定していることになるんだ。ここはまあわかんなくてもok!

下の行は,のちほど dump() という関数デバッグ用出力を可能にするための設定!そのた JavaScript コンソールにデバッグ情報を出力する方法もあるけど,その場合の設定は no title をみてね!

XUL ファイル

いよいよ chrome/content/main.xul ファイルを作る番だね!

% vi chrome/content/main.xul

中身は…

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>

<window id="main" title="Konnichiha Konnichiha" width="400" height="300"
 xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
  <script>
<![CDATA[
function start() {
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function () {
        if (xhr.readyState == 4 && xhr.status == 200) {
            view(xhr);
        }
    };

    xhr.open('get', 'http://b.hatena.ne.jp/hotentry');
    xhr.send(null);
}

function view(xhr) {
    dump(xhr.getAllResponseHeaders());
    document.getElementById('message').value = xhr.responseText;
}
]]>
  </script>
  <textbox id="message" multiline="true" rows="10" readonly="true"
           value="こんにちはこんにちは!"/>
  <button id="pushme" label="ひみつボタン" oncommand="start()"/>
</window>

これもコピペでオッケー!

ただし,文字コードUTF-8 で保存してね!

これは textbox の id をかえたくらいで,ほとんどはまちちゃんのコードのコピペだよ。JavaScript + DOM ってすばらしいね!

実行しよう!

端末を起動して,作業フォルダに移動…

% cd ~/tmp/myapp

さっきつくったやつを実行…!

% xulrunner application.ini

f:id:dayflower:20090119143617p:image

できた!やった!なんかうごいた!

ひみつボタンを押したら,はてなブックマークのソースコードが表示されたよー。

改造しよう!

XUL ファイルとか JavaScript は,Mozilla Firefox と同じやつが使われてるらしいですよ!

てことは,普段ホームページ作りの時に「あぁこれ IE だと使えないからなぁ」

なんて諦めていたやつが色々つかえるかもしれないですね!

XPath とか Canvas とか,なんかそういうの!

あと prototype.js とか jQuery みたいな便利ライブラリも普通に使えるよ!(たぶん)

やった!すごい!べんり!

だけど XUL アプリケーションのちょっとまずいところ

  • HTML のかわりに XUL とかいう謎なファイルを書かなくちゃいけない

謎なファイルって書いたけど,みんなも HTML を勉強するとき色々覚えたよね?

<input type="checkbox"> って書くとチェックボックスをだせるとか。

XUL の場合,その代わりにたとえば <checkbox> と書けばいいんだ。

つまり,新しいタグが増えたと思えばいいんだよ!

それに <colorpicker> とか素敵なウィジェットも揃っているよ!

くわしくは no title をみてね!

今回の Ubuntu Linux の場合,たいていは xulrunner はもともとインストールされている。

けど,Windows とかの場合は,ユーザに XULRunner をダウンロード*5させなきゃいけない。

(まぁ Windows の場合解凍するだけで大丈夫らしいし,Mac OS X の場合はインストーラ形式になってるよ)

2009-01-20 追記:

teramako

id:teramako XUL, xulrunner ネタを取られた...orz // 悔しいので一つネタを。xulrunnerじゃなくてもFx3からは firefox -app application.ini で起動できたりするよ

http://b.hatena.ne.jp/teramako/20090119#bookmark-11728256

ごめんなさいごめんなさい!

それはともかく有益な情報をありがとう!

Firefox 3 だと,わざわざ xulrunner をインストールしなくても,

Firefox のバイナリから XUL アプリケーションを動かせるんだね。

これでアプリケーション配布の間口がちょっと広くなるね!

2009-01-20 追記おわり

AIR でもランタイムインストールさせる必要はあるけど,

アプリケーションの配布と同時にインストールも行うようにできるよ。

そもそも AIR の場合,アプリケーションの標準的な配布方法が定まっているのが大きいね!

参考になるページ

公式のページに参考になることが色々書いてあるよ!

みんな作って、どんどこ公開しちゃえばいいんじゃないかな!

(できれば、ぼくが見て勉強できるようにソースコード付きで…!)

おわりに

いわずとしれてるけど,これは下記のすばらしい記事へのオマージュだよ!

あとは私感だよ!

*1:原典をみればわかるけど,WindowsMac OS X で動かすのも,たいして難しくはないよ!

*2Firefox 3 が入っていれば,だけど

*3:これはいかにも必須じゃなさそうだけど,XULRunner にメインウィンドウとなる XUL ファイルの名前をわたすために使います。

*4:これは chrome/ フォルダからの相対パス指定だよ。

*5http://releases.mozilla.org/pub/mozilla.org/xulrunner/releases/1.9.0.5/runtimes/ とかにあるよ。

*6HTML でいうところの &amp; みたいな書き方のことだよ!XUL だとこの DTD を自分で用意することで,自分オリジナルな実体宣言ができるんだ。あとは,その参照先は英語の場合「foobar」だよ,みたいに DTD に書けば,世界中のみんなから使ってもらえるアプリケーションになるよ!

2008-12-19

CPAN 最速検索の劣化コピー作ってみた

mala さんの CPAN 最速検索を便利に使わせてもらってるんですが,操作上ちょっと不満なところがいくつかありまして。

  • カーソルキーのオートリピートがきかない
  • ホイールがきかない
  • 候補の同時表示数が固定

Firefox だからかもしれないですけど。

んで改造しようと思ったんですがわたしにはちと難しそうだったのでいっそ自分なりに書いてみようと思いました。もちろんライブラリを使わずに書く技能はないので jQuery を使いました。

標準機能だとマウスホイールをトラップするのがたいへんそうだったのでプラグインも使いました。


mala さんのコードをチラ見すると

してる感じでした。

私はディストリビューション(というか親パッケージ?)の情報はいらないので,なんとかパッケージ一覧を自力で作成できればなんとかなりそうだなと思いました。


cpan を実行する時によくでてくる 02packages.details.txt とかいうのにパッケージ全リストがあるみたい。

File:         02packages.details.txt
URL:          http://www.perl.com/CPAN/modules/02packages.details.txt
Description:  Package names found in directory $CPAN/authors/id/
Columns:      package name, version, path
Intended-For: Automated fetch routines, namespace documentation.
Written-By:   Id: mldistwatch.pm 1080 2008-12-16 04:08:35Z k 
Line-Count:   61204
Last-Updated: Wed, 17 Dec 2008 03:27:54 GMT

AAA::Demo                         undef  J/JW/JWACH/Apache-FastForward-1.1.tar.gz
AAA::eBay                         undef  J/JW/JWACH/Apache-FastForward-1.1.tar.gz
AAC::Pvoice                        0.91  J/JO/JOUKE/AAC-Pvoice-0.91.tar.gz

...... snip snip snip ......

このヘッダをとばして,先頭の文字列を切り出せばパッケージになりそう。

てことで,

#!/usr/bin/perl
# - package.pl

use strict;
use warnings;

while (<>) {
    chomp;
    last if $_ eq q{};
}

print "[\n";
while (<>) {
    chomp;
    s{ \s .* \z }{}xmso;

    print q{"}, $_, q{"}, q{,}, qq{\n};
}
print "]\n";

捨てスクリプトなのでかなり適当ですが,こいつを 02packages.details.txt にたいして実行すると,

[
"AAA::Demo",
"AAA::eBay",
"AAC::Pvoice",
// ...... snip snip snip ......

みたいになるのでこれを JSON で読み込めばよろしいと。


あと,Perlバンドルされてるドキュメント…… perlsyn.pod とかも見れるようにしたほうが便利だよね,ということで,perl.pod からマニュアル一覧を取得して配列で吐くスクリプトも書きました。

#!/usr/bin/perl
# - bundles.pl

use strict;
use warnings;

my @modules;

my $started;
while (<>) {
    chomp;

    if (! $started) {
        if (m{ \A =head2 }xmso) {
            $started = 1;
        }
        next;
    }

    last if m{ \A =head1 }xmso;
    next if m{ \A =head  }xmso;

    last if m{ \A \S }xmso;

    if (m{ \w+ }xmso) {
        push @modules, $&;
    }
}

print "[", "\n";
print join qq{\n}, map { qq{"$_",} } sort @modules;
print "\n", "]", "\n";

こいつを実行すると,

[
"perl",
"perl5004delta",
"perl5005delta",
// ...... snip snip snip ......

みたいになります。


んでよくよく考えたら,全パッケージ情報を一括で読み込むのなら,Ajax で遅延ロードする必要ないんじゃね?と思って,<script> タグで読み込むようにしました。いままでに生成した2つの JavaScript ファイルに変数宣言もつけて一つのファイルに吐くように Makefile を書いてみました。

SITE=http://ftp.kddilab.jp/CPAN/

TARGET1=packages.js
SOURCE1=02packages.details.txt.gz
TARGET2=bundles.js
TARGET3=vars.js
TARGETS=$(TARGET1) $(TARGET2) $(TARGET3)

all:		$(TARGETS)

clean:
	rm -f $(TARGETS)

$(TARGET3):	$(TARGET1) $(TARGET2)
	echo "var packages_static =" >  $@
	cat $(TARGET1)               >> $@
	echo ";"                     >> $@
	echo "var bundles_static ="  >> $@
	cat $(TARGET2)               >> $@
	echo ";"                     >> $@

$(TARGET1):	$(SOURCE1)
	zcat $< | perl packages.pl > $@

$(TARGET2):
	cat `perldoc -ml perl.pod` | perl bundles.pl > $@

$(SOURCE1):
	wget $(SITE)modules/$(SOURCE1)

これで結合したファイル vars.js ができる,と。


あとはノリで JavaScript コーディングしていきました。ほんとは構造化して書いたらかっこいいんでしょうけど,めんどうだったのでかなりフラットに書いてます。

JavaScript のソースはこちら→ 502 Bad Gateway

jQuery を使ってるのに documente.getElementById() してたりするのは,理想的には jQuery に依存したくないというのがあったのと,ID 指定で単一のエレメントが返るとわかってるセレクタにたいして $('#hoge').get(0) とか $('#fuga')[0] とか書くより(体感できないとは思いますが)速いかなと思ってそうしてます。とはいってもイベントまわりとかブラウザ間非互換性を埋める上で jQuery にかなりお世話になっています。


いままででてきた登場人物を使う index.html がこちら→ 502 Bad Gateway

実際には css ファイルは分離してますが例なので組み込んでます。あと Yahoo UIreset.cssfonts.css も使っています。

で,出来上がってから元記事を読んでみた

省メモリ、高速に動作するように工夫してあります。

  • 配列に変換せずに一つの巨大な文字列から検索 → 切り出し。
  • クロージャで次の検索結果を取得する関数を保持しておいて、描画が必要になった時点で検索を実行。
    • その代わりにトータルのヒット件数がわかりません。
最速インターフェース研究会 :: CPAN最速検索

な,なるほどー。

わたしのコードでは

  • 配列として読み込んでいる
    • 巨大文字列より大量にメモリを消費してそう。最初に遅延してそう。
  • 検索をおこなうと,都度都度検索結果を配列に格納してます
    • 検索をおこなうたびメモリと実行時間を消費
    • そのかわりヒット件数をだせた
  • カーソルを動かすたび,再描画
    • 遅いマシンだとちらつく可能性あり

のように富豪的になってます。

そのかわり自分で手をいれられるのでトレードオフとしてまあいいか,と。PageUp,PageDown,Ctrl+Home,Ctrl+End など使いたいキーバインドも使えるようになりましたし。あと iframe で pod page を開く機能もつけてます。デフォルトで disable してますけど。


お勉強になりました。これはないわーというところがあったら遠慮なくご指摘ください。

よくある質問と答え

  • XSS がある気がしますが
    • モジュール名にヤバい文字列がないことを前提として書いてます。すいません。
    • 自分使いなのでいいかと思った。
  • IE で動きません
    • すいません。普段つかいが Firefox なもので Firefox でしか確認してません。
    • 一応そこそこ動くように調整を重ねましたが,キーボードによるセレクションがまったく動作しません。

2008-12-09

はてブコメントの並び順を変えるグリモン書いた

はじめて Greasemonkey 書きましたよ。おかしなところがあったら教えてください。

さいしょ はてなブックマークのコメントを昇順に並べ替えるGreasemonkey を使ってたんですけど,いろいろ不満があったので書き換えてみました。原型はとどめてない。

light じゃない版あったのカー→no title。じゃあこれいらないや。

  • ページを読み込んだ段階では,ソートボタンを付与するだけで何もしない
    • 問答無用でソートするとコメント数が多いページで待ちが多くなるので
  • 何種類かのボタンを指定できる
  • ソート順を指定する関数を指定できる
  • ソート中は「ソート中」って出る

まぁ「指定できる」ってのは,コード(setup() 部分)書き換えてくださいねっていうレベルなんですけれど。

ほんとは no title とか使えばいいんでしょうけど,結構ブクマの entry ページとかよく見るんですよ。

// ==UserScript==
// @name           Sort HB Comment 2
// @namespace      http://d.hatena.ne.jp/dayflower/
// @description    Sort Hatena Bookmark Entry Page
// @include        http://b.hatena.ne.jp/entry/*
// @include        http://b.hatena.ne.jp/entry/?mode=more&url=*
// ==/UserScript==

(function () {

setup([
    {   // 逆順 (コメント優先)
        label: '\u25bd\u9006\u9806',
        criteria: function (a, b) {
            // first criteria: comment
            var ac = a.getAttribute('class') || '';
            var bc = b.getAttribute('class') || '';

            if (bc.match(/nocomment/)) {
                if (! ac.match(/nocomment/))
                    return -1;
            }
            else {
                if (ac.match(/nocomment/))
                    return 1;
            }

            // second criteria: original order (reverse)
            return b.xHatebuOrder - a.xHatebuOrder;
        }
    },
    {   // 元の順序
        label: '\u25bd\u6b63\u9806',
        criteria: function (a, b) {
            return a.xHatebuOrder - b.xHatebuOrder;
        }
    }
]);

var items;      // GLOBAL

function setup(confs) {
    var marks = document.getElementById('bookmarked_user');
    if (! marks)
        return;
    if (marks.childNodes[1].textContent
                     // 非表示に設定
             .match(/\u975e\u8868\u793a\u306b\u8a2d\u5b9a/))
        return;

    var res = document.evaluate(
        '//h2[@class="comment bookmark-list"]/span/span[@class="count"]',
        document.body,  null, 7, null
    );
    if (res.snapshotLength <= 0)
        return;
    var area = res.snapshotItem(0);
    for (var i = 0, n = confs.length; i < n; i ++)
        add_button(area, confs[i]);
}

function init_items() {
    var marks = document.getElementById('bookmarked_user');
    var items = [];

    if (1) {
        var t = marks.getElementsByTagName('li');

        for (var i = 0, n = t.length; i < n; i ++) {
            items[i] = t[i];
            items[i].xHatebuOrder = i;
        }
    }
    else {
        var res = document.evaluate('li', marks,  null, 7, null);

        for (var i = 0, n = res.snapshotLength; i < n; i ++) {
            items[i] = res.snapshotItem(i);
            items[i].xHatebuOrder = i;
        }
    }

    return items;
}

function do_sort_comments(criteria) {
    if (items == null)
        items = init_items();

    items.sort(criteria);

    var marks = document.getElementById('bookmarked_user');

    var newlist = document.createElement('ul');
    newlist.setAttribute('class', marks.getAttribute('class'));

    for (var i = 0, n = items.length; i < n; i ++)
        newlist.appendChild(items[i]);

    newlist.setAttribute('id', marks.getAttribute('id'));
    marks.parentNode.replaceChild(newlist, marks);
}

function add_button(area, conf) {
    var link = document.createElement('a');
    link.style.cursor = 'pointer';
    link.style.color  = '#fff';

    link.innerHTML = conf.label || '\u25bd';

    if (conf.title)
        link.title = conf.title;

    link.addEventListener('click', function (e) {
        var me = e.target;

        //              TEXTNODE
        var backup = me.firstChild.nodeValue;
        //                         ソート中…
        me.firstChild.nodeValue = '\u30bd\u30fc\u30c8\u4e2d\u2026';

        window.setTimeout(
            function () {
                do_sort_comments(conf.criteria);
                me.firstChild.nodeValue = backup;
                //me.parentNode.style.visibility = 'hidden';
            },
            0
        );

        e.preventDefault();
    }, true);

    var button = document.createElement('span');
    button.appendChild(document.createTextNode('\u3000['));
    button.appendChild(link);
    button.appendChild(document.createTextNode(']'));

    area.appendChild(button);
}

はじめて Greasemonkey 書いて感じたこと。

  • デバッグがめんどくさい
    • エラー時に firebug console に出力される内容がちょっと変だったり。環境依存?
  • でも DOM 操作とか非常に勉強になる
  • Firefox でしか動かない JavaScript を書くくせがつきそう
  • 自作スクリプトインストールがめんどくさい
    • ドラッグドロップで登録できないんですけど…… Linux だから?
  • HTMLCollection の sort() ができなかったんで,Array にコピーしてます
    • Array.prototype.sort 使ってみたけどうまくいかなかったです

2008-10-14

Web Developer 1.1.6 日本語版の(ツールバーの設定等の)不具合

ミナトラボさんで配布されている Web Developer 日本語版 機能拡張は,ツールバーのアイコンの設定が保存されない(つねにアイコンとラベルが表示される)などの不具合があります(わたしのところだけ?)。

詳しい原因ははぶきますけれど,拡張機能の xpi ファイルや内部 jar ファイルを展開していって,locale/ja-JP/webdeveloper/webdeveloper.properties ファイルの

webdeveloper_validateHTML=HTML構文の検証 (W3C:英語)
webdeveloper_validateLinks=リンク切れの検証 (W3C:英語)

となっているところを

webdeveloper_validateHTML=HTML構文の検証 (W3C:英語)
webdeveloper_validateHTMLbyHTMLLint=HTML構文の検証 (HTMLLint:日本語)
webdeveloper_validateLinks=リンク切れの検証 (W3C:英語)

のように,webdeveloper_validateHTMLbyHTMLLint を追加すれば支障なく動くようになります。


あるいは webdeveloper.jsfunction webdeveloper_setupLocalizedOptions() の中の,

        // HTMLLint
        webdeveloper_setStringPreferenceIfNotSet("webdeveloper.tool.1.description", stringBundle.getString("webdeveloper_validateHTMLbyHTMLLint"));
        webdeveloper_setStringPreferenceIfNotSet("webdeveloper.tool.1.url", "http://openlab.ring.gr.jp/k16/htmllint/htmllint.cgi?Stat=on&ViewSource=on&Method=URL&URL=");

あたりを削除して自分で「ツールの編集」からやったほうが一貫性という面ではいいかもしれません。


ようするに英語版になかったメニューを追加してロケールの追加を忘れてるんで stringBundle.getString() で落ちています。


とゆーか,今回の件とは関係ありませんが,ここのあたり,元コードの時点でロジックがまずい気がします(たとえばツールをいくつか削除しても復活しちゃうんじゃないかな)。

2008-10-07

jQuery でページスクロール

jQuery 1.2 以降だと scrollTop(と scrollLeft)という疑似スタイルが利用できるようになったので,jQuery UI を使わずとも,jQuery Core 本体だけでページ内スクロールができるようになりました。

たとえば,

<html>
    <body>
        <p>
            blah, blah, blah, ...
            blah, blah, blah, ...
        </p>

        <p>
            <a href="#" id="link_to_top">ページの先頭へ</a>
        </p>
    </body>
</html>

このような HTML で「ページの先頭へ」というリンクをクリックしたときに,スムーズにスクロールしたいのなら,

$(function () {
    if (! $.browser.safari) {
        $('#link_to_top').click(function () {
            $(this).blur();

            $('html,body').animate({ scrollTop: 0 }, 'slow');

            return false;
        });
    }
});

のように記述するだけで OK です*1

トップに移動したときにロケーションバーの URL# 付きになるのがイヤ & デフォルト動作を回避するために return false; で帰ってます。

jQueryscrollTop 疑似スタイルとは?

DOM における scrollTop プロパティのことではありません。

jQuery Core の 汎用アニメーション関数animate() って,アニメーションのターゲットとして指定できるのがスタイルだけなんです。

で,たとえばブロックのスライドダウンなどの場合,スタイルの height をいじればすみます。しかしスクロールのポジションについてはスタイル側にプロパティが用意されていません(DOM 側の scrollTop プロパティで操作する)。それだと不便だよね,ということで,jQuery 1.2 では css 側の関数として scrollTop() などを用意したそうです。詳しくは Milestone 1.2 ? jQuery - Bug Tracker を参照してください。

ちなみに DOM インタフェース経由でスタイルを直接いじっても反映されるというわけではないです。あくまで jQuerycss()animate() などの関数経由で指定できるようになったということです。また直接値を取得・設定する scrollTop()scrollLeft() といった関数もあります。

なぜ $('html,body') と指定しているのか

最初 jQuery ドキュメントの例 の通り,$('body') とだけ書いていてうまくいかないよーと悩んでいました。

んで,検索したら下記のページがひっかかりました。

But why do we need to select both body and html? Well, Firefox and IE use body in quirks mode but html in standards mode. Our $('html, body') selector takes both situations into account. Of course, if you know your pages are running in standards mode (which they should), then you can drop the body (and the comma) from the selector.

Animated Scrolling with jQuery 1.2 | Learning jQuery

ようするに,FirefoxIE では,後方互換性モードの場合は <body>scrollTop を指定するべきであるのに対して,標準準拠モードの場合は <html> のそれを指定するべきだということだそうです。なので,念のために html,body と両方指定すればいずれのモードでも動くようにできます。

イージングしたい

上記のコードだと,リンクをクリックするとぬるぬるとスクロールしていきます。これを改善するのがイージングです。

イージングを使うと,たとえば初速はそれなりの速度で着地するにはふわっとさせたり,逆に初速はゆるゆると動くけれどだんだんスピードがあがったり,などさせることができます。つまりアニメーション速度を関数で制御できるようにする機構のことです。

jQuery Core には linearswing という2つのイージング関数が登録されているのですが,jQuery Easing Plugin を使えばもっといろんなイージングができます。

このプラグイン全体を読み込んでもいいのですが,実際に利用するのは1つか2つくらいのものでしょう。その場合,ソースから必要な関数だけコピペすれば充分です。

たとえば,

	easeOutQuart: function (x, t, b, c, d) {
		return -c * ((t=t/d-1)*t*t*t - 1) + b;
	},

このイージング関数を利用したい場合,以下のようにコピペすれば*2最小限のソースで利用することができます。

jQuery.easing.quart = function (x, t, b, c, d) {
    return -c * ((t=t/d-1)*t*t*t - 1) + b;
};  

$(function () {
    $('#link_to_top').click(function () {
        $('html,body').animate({ scrollTop: 0 }, 300, 'quart');
    });
});

応用

今回の例だとページトップへのリンクのみスムーススクロールするようにしましたけれど,他のアンカーへ飛ぶときにも利用するのもオツでしょう。

先ほど引用した Animated Scrolling with jQuery 1.2 | Learning jQuery からの例ですが,

var targetOffset = $('#hogehoge').offset().top;
$('html,body').animate({scrollTop: targetOffset}, 1000);

のようにターゲットの絶対位置を offset() 関数で取得して,0 の代わりにそれを scrollTop に指定すれば,ターゲットまでスクロールさせることも可能です。


さらに,このページには $('a[href*=#]') で全アンカーへのリンクを取得して自動的にスクロールするように適用するコードも載っています。また,html(や body)だけでなく,overflowscroll な要素に対して適用する例も載っています。

*1:Safari ではうまくアニメーションしないそうなので,ブラウザ避けをはさんであります。でもよくよく考えたらアニメーションしないだけでズバっと移動はしてくれるらしいのでブラウザ避けするまでもなかったかも。

*2jQuery Easing Plugin は BSD License なので,実地で使うにはソース冒頭のライセンス表記もコピーしたほうがよいです。

2008-09-12

V8 で C++ から JS Object のプロパティを列挙したい

C++ で V8 を拡張する関数とか書いていると,JavaScript から Object(というか,今回のコンテキストではざっくりいうと Hash 的なもの)をわたしてあれこれしたい,という欲求がでてきます。たとえば Object から apr_table_t に変換したい,とかね。


もっと単純に,

var hash = {
    field1: false,
    field2: 1,
    field3: 'abc'
};

// show_props(hash);
//      /*
//          みたく C++ の関数 show_props を呼びたい;
//          以下のようなことをする関数ね
//       */

for (var key in hash) {
    System.out.println(key);
}

みたいなコードを動かしたいとします。

ところが v8.h での class Object のインタフェースを眺めてみても,プロパティの列挙に使えそうなインタフェースはありません。

うーむと思って,Issue リストを眺めてたら……

Reported by matt...@trebex.net, Sep 06 (5 days ago)

There doesn't seem to be a way of listing the properties of an object from

C++, without evaluating a for..in loop.

---

Comment 1 by christian.plesner.hansen, Sep 08 (3 days ago)

We should have an API function for enumerating properties.

no title

ええ,まったくもって you should have でございます。


ただ,同情というかなるほどなぁと思ったのですが,プロパティの列挙に関して ECMA-262 の仕様を調べてみると,12.6.4 The for-in Statement でちらりとでてくるのみです。

生成規則 IterationStatement : for ( LeftHandSideExpression in Expression ) Statement は、次のように評価される:

  1. Expression を評価。
  2. GetValue(Result(1)) を呼出す。
  3. ToObject(Result(2)) を呼出す。
  4. V = empty とする。
  5. DontEnum 属性を持たない、 Result(3) の次のプロパティの名前を取得する。そのようなプロパティが存在しないならば、 ステップ 14 へ。

以下略

12.6.4 The for-in Statement

プロパティの列挙に関わるのは「Result(3) の次のプロパティの名前を取得する」という文しかないです。だから内部的な実装しかないというのは,まぁ理解できます*1


ともかく,ない袖は振れないので,はてなブログ で紹介されている Google グループ をもとに,プロパティを列挙する関数JavaScript で書いて,それを C++ 側から呼び出すことにしました。

プロパティを列挙する関数といってもたいしたことはなくて,

function _enum_properties_(v) {
    var a = [];

    for (var k in v)
        a.push(k);

    return a;
}

こんな感じのものです。ENUMerable じゃないプロパティを取得できないとか,プロトタイプチェーンをたどってキーを列挙しちゃうよ(ですよね……たしか)とか問題はありますが,とりあえずこれでも C++ から呼べたら便利かな,と。


この JavaScript 関数をエンジンに登録するソースは以下のような感じです。

static Handle<Value> execute_source(Handle<Context>, const char *);

static const char *
enum_properties_source(void)
{
    return
        "function (v) {"                "\n"
        "	var a = [];"            "\n"
        "	for (var k in v)"       "\n"
        "		a.push(k);"     "\n"
        "	return a;"              "\n"
        "};"                            "\n"
        ;
}

static Handle<Value>
enum_properties_function(Handle<Context> context)
{
    return execute_source(context, enum_properties_source());
}

static bool
register_enum_properties(Handle<Context> context)
{
    Handle<Value> enum_properties
        = enum_properties_function(context);

    if (enum_properties.IsEmpty()) {
        fputs("failed to register enum_properties.\n", stderr);
        return false;
    }
    context->Global()->Set(String::New("_enum_properties_"), enum_properties);

    return true;
}

static Handle<Array>
call_enum_properties(Handle<Object> any, Handle<Value> target)
{
    Handle<Function> func
        = Handle<Function>::Cast(any->Get(String::New("_enum_properties_")));

    Handle<Value> argv[1] = { target };

    Handle<Value> result = func->Call(func, 1, argv);
    return Handle<Array>::Cast(result);
}

Context のグローバルオブジェクト(のテンプレート,ではないことにやや注意)に,_enum_properties_ としてさきほどの関数を登録しています。なので,この関数を呼び出すときは,「なんかしらのオブジェクト」から「_enum_properties_」を取得すれば,(結果的にプロトタイプチェーンのルートたるグローバルオブジェクトから)ひっぱってこれます。上記の引数では Handle<Object> any を要求しています。C++ の InvocationCallback から呼び出す場合,ArgumentsThis() あたりを与えてやればよろしい。

返り値は Handle<Array> なので Length() も使えるし,数値インデックスで値を Get() することもできます。これで C++ からまともに扱えるようになった,と。


残りのソースです。

#include <stdio.h>
#include <v8.h>
using namespace v8;

static Handle<Value>
execute_source(Handle<Context> context, const char *source)
{
    //HandleScope scope;
    TryCatch try_catch;

    Context::Scope context_scope(context);

    Handle<Script> script
        = Script::Compile(String::New(source), Undefined());

    if (script.IsEmpty()) {
        String::AsciiValue error(try_catch.Exception());
        fprintf(stderr, "compile error: %s\n", *error);
        return Undefined();
    }
    else {
        Handle<Value> result = script->Run();

        if (result.IsEmpty()) {
            String::AsciiValue error(try_catch.Exception());
            fprintf(stderr, "execute error: %s\n", *error);
            return Undefined();
        }
        else {
            return result;
        }
    }
}

/*
    このへんにさきほどの enum_properties まわりをいれる
 */

static Handle<Value>
show_props_(const Arguments &args)
{
    if (args.Length() < 1)
        return Undefined();

    Handle<Array> props = call_enum_properties(args.This(), args[0]);

    for (uint32_t i = 0; i < props->Length(); i ++) {
        fprintf(stdout, "[%u]: '%s'\n",
                    i,
                    * String::AsciiValue(props->Get(Uint32::New(i)))
        );
    }

    return Undefined();
}

int
main(int argc, char *argv[])
{
    HandleScope scope;

    Handle<ObjectTemplate> global = ObjectTemplate::New();

    global->Set(String::New("show_props"), FunctionTemplate::New(show_props_));

    Handle<Context> context = Context::New(NULL, global);
    Context::Scope context_scope(context);

    if (! register_enum_properties())
        return 1;

    Handle<Value> result
        = execute_source(
            context,
            "var hash = {"                      "\n"
            "    field1: false,"                "\n"
            "    field2: 1,"                    "\n"
            "    field3: 'abc'"                 "\n"
            "};"                                "\n"
                                                "\n"
            "show_props(hash);"                 "\n"
        );

    if (result.IsEmpty())
        return 1;

    return 0;
}

このサンプルでは show_props_() という C++ の関数で Object のプロパティ列挙を使っています。

実行すると,

% ./enumprops

[0]: 'field1'
[1]: 'field2'
[2]: 'field3'

無事列挙できました。


なんともまわりくどい手ですね。はやくきちんとした API が実装されるといいなぁー。

*1:V8 のコードのインタフェースは,意外にも?実直に ECMA-262 の仕様に合わせてあります。

2008-09-08

V8 (Google JavaScript Engine) を embed した感想とかあれこれ

なぜ CodeRepos に登録しないのか

べつだん深意や確執があるわけじゃなくて,華々しく 500 人めのコミッタになろうと思ったら,現在 461 人だったからです。ということで 38 人の方々,コミッタ登録してください ;P

冗談はともかくおまえの書いた汚いコードを早く添削したいんじゃという方がいらっしゃったら,代理でいれといて構いません。

Acme::JavaScript::V8Perl XS)を書くときに苦労したこと

New ていうのが XS での define 値だったので困りました。V8 側だと,new / delete するんじゃなくて Class::New() する流儀なので。

ですから,#undef New してあります。他のマクロで使われていたらマズいなぁと思いますが,動いたからよしとします。

エンベッダーズガイド

Handle<T> とか Local<T> とか Persistent<T> とか

Handle<T> とかっていうのは,いわゆるスマートポインタ的なものです。

なお Handle<T> が抽象クラスで,Local<T>Persistent<T> はそれらのサブクラスになります。だからたいていの場合,自作関数の戻り値は,Handle<T> にしとけばいいでしょう。

Local<T>HandleScope によって自分でスコープを指定できる auto_ptr 的なものです。実際には参照されているかどうかも加味してくれる(被参照がなくなれば GC される)ので,関数(スコープ)抜けたらなくなるのかー,とビビる必要はありません。たぶん。

Persistent<T> は,いちど V8 の世界を抜けてもそのハンドルを保持しとく必要がある場合に使うものであり,必要がなくなったら自分で Dispose() する必要があります。基本的に使う必要はありません。V8 のほとんどの API(*::New() とか)の戻り値は Local<T> ベースですし。

Acme::JavaScript::V8 の場合だと,

  • コンテキスト生成時に Persistent<T> で保持,Perl インタプリタに制御を返す
  • そのコンテキストを利用してあれこれ(コンパイルしたり実行したり)するときに使う変数は,Local<T>

つまりほとんど Local<T> です。

mod_v8pp の場合だと,リクエストごとにコンテキストを生成,ソースコンパイル,実行,を行っているので Local<T> しか使っていません。

そのたあれこれ

  • そんな便利な Handle<T> 系テンプレートですが,自作クラスをその対象とすることはほぼ無理
    • プリミティブは V8 library が自力でヒープから取得して初期化したりしてる
    • それで GC の対象にしてる
  • コンパイル,というのは,字句的なコンパイルだけやってるわけではないっぽい

後者はどういうことか,というと,

var a = 1;
var b = a + c;      /* 'c' is not defined (compile time error) */
foo();              /* 'foo' is not defined (compile time error) */

これらがコンパイル時にエラーになります。同一コンテキストで,

var d = a + 5;

をコンパイルする場合はエラーにはなりません(a は同一コンテキストで先ほど定義されたから)。

関数変数が定義されているかどうかを実行フェーズで判断するインタプリタ処理系が多いんですけど(そのほうが実行時のダイナミズムを確保しやすいから),ちょっと意外でした。


実使用上は,コンテキスト生成時に指定するグローバル ObjectTemplateNamedPropertyGetter 等を利用すると,なんとかなるのかなぁ。

Object に内部的な値を格納する方法

v8::Objectに内部保持するnativeな値(例えばハンドルとか)を保持するには

    FILE* fp = fopen(...);
    v8::Local<v8::Object> obj = v8::Object::New();
    obj->Set(v8::String::New("value"), v8::External::New((void*)fp));

という様に、v8::Externalでメンバを追加してやれば良い。

Big Sky :: javascript v8エンジンでv8::Objectに内部的な値を格納する方法が分かった。

もちろんそれで動くんですけど,外にみせたくない内部値を扱うには,ObjectTemplate の InternalField を利用するほうが外にさらされないので better だと思います。XS の MAGIC EXT みたいな感じです。

ファイルハンドルを操るばあいの実例。ほんとは close の際に InternalField[0] を NULL にしたり,妥当性をチェックするべきなんですが,とりあえず。

#include <stdio.h>

#include <v8.h>
using namespace v8;

static Handle<Value>
_p(const Arguments& args)
{
    HandleScope scope;
    String::AsciiValue str(args[0]);
    fputs(*str, stdout);
    return Undefined();
}

static Handle<Value>
_open(const Arguments& args)
{
    HandleScope scope;

    if (args.Length () != 2)
        return ThrowException(String::New("usage: open(fname, mode)"));
    String::AsciiValue fname(args[0]);
    String::AsciiValue mode(args[1]);

    FILE *fh = fopen(*fname, *mode);
    if (! fh)
        return ThrowException(String::New("cannot open file"));

    args.This()->SetInternalField(0, External::New(fh));

    return args.This();
}

static Handle<Value>
_close(const Arguments& args)
{
    HandleScope scope;

    Local<Value> intl_field = args.This()->GetInternalField(0);
    FILE *fh
        = reinterpret_cast<FILE *>(Handle<External>::Cast(intl_field)->Value());

    fclose(fh);

    return True();
}

static Handle<Value>
_gets(const Arguments& args)
{
    HandleScope scope;
    static char buffer[1024];

    Local<Value> intl_field = args.This()->GetInternalField(0);
    FILE *fh
        = reinterpret_cast<FILE *>(Handle<External>::Cast(intl_field)->Value());

    if (! fgets(buffer, 1024, fh)) {
        return Undefined();
    }

    return String::New(buffer);
}

static Handle<FunctionTemplate>
create_file_object_template(void)
{
    HandleScope scope;

    Handle<FunctionTemplate> ft = FunctionTemplate::New();
    Handle<ObjectTemplate>   ot = ft->InstanceTemplate();

    ot->Set(String::New("open"),  FunctionTemplate::New(_open));
    ot->Set(String::New("close"), FunctionTemplate::New(_close));
    ot->Set(String::New("gets"),  FunctionTemplate::New(_gets));

    ot->SetInternalFieldCount(1);

    return ft;
}

int
main(int argc, char *argv[])
{
    HandleScope scope;
    TryCatch try_catch;

    Handle<ObjectTemplate> global
        = ObjectTemplate::New();

    // debugging purpose
    global->Set(String::New("p"), FunctionTemplate::New(_p));

    global->Set(
        String::New("File"),
        create_file_object_template(),
        PropertyAttribute(ReadOnly | DontDelete)
    );

    Handle<Context> context = Context::New(NULL, global);

    Context::Scope context_scope(context);

    Handle<String> source = String::New(
        "var f = new File();\n"
        "\n"
        "f.open(\"test.txt\", \"r\");\n"
        "var s;\n"
        "while (s = f.gets()) {\n"
        "    p(s);\n"
        "}\n"
        "f.close();\n"
    );
    Handle<Script> compiled = Script::Compile(source, Undefined());

    if (compiled.IsEmpty()) {
        String::AsciiValue error(try_catch.Exception());

        fprintf(stderr, "compile error: %s\n", *error);
    }
    else {
        Handle<Value> result = compiled->Run();

        if (result.IsEmpty()) {
            String::AsciiValue error(try_catch.Exception());

            fprintf(stderr, "execute error: %s\n", *error);
        }
        else {
        }
    }

    return 0;
}

destruction 時に自動的に close() するようにしたかったんですが,やりかたがわかりませんでした*1。とりあえず明示的に close() してください。

いったん FunctionTemplate を作ってそこの InstanceTemplate を利用している理由は,JavaScript 側で var f = new File(); みたいに関数オブジェクトを利用した new を利用させる(ほうが見目麗しい)からです。

new する必要がない場合,たとえば,すでに存在する外部リソースJavaScript で扱う場合は,直接 ObjectTemplate::New() すればいいです(もちろん FunctionTemplate 経由でもいいんですけど)。

サンプルとして Apacheapr_table_t * 型の(ハッシュ)テーブルを V8 JavaScript で扱う例をあげます。ちまちま JavaScript Array を構築してもいいんですけど,ObjectTemplate を利用するとプロパティアクセサを自分で指定できるので,いい感じです。

static Handle<Value>
apr_table_property_getter(Local<String> property, const AccessorInfo& info)
{
    Handle<Value> intl_field = info.This()->GetInternalField(0);
    apr_table_t *table
        = (apr_table_t *) Handle<External>::Cast(intl_field)->Value();

    String::AsciiValue key(property);

    const char *value = apr_table_get(table, *key);

    if (value)
        return String::New(value);
    else
        return Undefined();
}

/******* snip snip snip *******/

static Handle<ObjectTemplate>
create_apr_table_ro_template(void)
{
    Handle<ObjectTemplate> ot = ObjectTemplate::New();

    ot->SetNamedPropertyHandler(apr_table_property_getter,
                                NULL,
                                apr_table_property_query,
                                NULL,
                                apr_table_property_enumerator);

    ot->SetInternalFieldCount(1);

    return ot;
}

static Handle<Object>
create_apr_table(Handle<ObjectTemplate>ot, apr_table_t *table)
{
    Handle<Object> object = ot->NewInstance();
    object->SetInternalField(0, External::New(table));

    return object;
}

/******* snip snip snip *******/

void sample(request_rec *r);
{
    apr_table_t *table = r->headers_in;

    Handle<ObjectTemplate> apr_table_ro_template
        = create_apr_table_ro_template();

    Handle<Value> headers_in
        = create_apr_table(apr_table_ro_template, table);

    /*
        これで headers_in を JavaScript で扱えるようになりました
     */
}

本筋とは関係ないですが,SetNamedPropertyHandler() するときに任意(といっても Handle<Value> 型)の Data をわたせます。が,あくまで対象は ObjectTemplate です。だから,あんまり用途はありません(いくつかの違う種類の class を同じ callback で利用する場合くらい)。各インスタンスごとの秘密データは,さきにあげたように InternalField 経由で設定しておくべき。

*1Persistent<Object>::MakeWeak() するのもなんか違うような気が……。SetGlobalGCPrologueCallback() でなんとかするのかなぁ。それも違うよなぁ。