vue-codemirror を試す

CodeMirror っていう JavaScript 製のエディタコンポーネントの Vue.js 用コンポーネント vue-codemirror を触ってみたときの備忘録。

最終的なコードはここにある。

github.com

vue-codemirror をプロジェクトに読み込む

import Vue from 'vue'
import VueCodemirror from 'vue-codemirror'

import 'codemirror/lib/codemirror.css'

Vue.use(VueCodemirror)

こんな感じ。

これを plugins/vue-codemirror.js に切り出して、 main.js

import '@/plugins/vue-codemirror'

コンポーネントの利用

ドキュメント通りでできるけど、このあとのベースラインとして。

<template lang="pug">
  #app
    codemirror(v-model="code" :options="cmOptions")
</template>

<script>
export default {
  name: 'App',

  data() {
    return {
      code: '',

      cmOptions: {
      }
    }
  }
}
</script>

インデントまわりのカーソル移動をいい感じにする

これでだいたいエディタっぽいアプリをつくることはできるんだけど、インデントまわりに不満がある。個人的には以下のようにしたい。

  • インデントはタブではなく、ホワイトスペースを利用してほしい
  • インデントの先頭でバックスペースを押したとき、文字単位ではなくインデント単位で直前の空白を削除してほしい

CodeMirror のオプションを指定したらいい感じになるかなと思ったけど、うまくいかなかった。調べたら以下のような先達の tips があった。

Setting for using spaces instead of tabs · Issue #988 · codemirror/CodeMirror · GitHub

しかしこの通りにやってもうまくいかなかったので、ベースを利用しつつ、自分の好みになるように書いてみた。

Vue.use(VueCodemirror, {
  options: {
    // based on https://github.com/codemirror/CodeMirror/issues/988#issuecomment-549644684
    extraKeys: {
      Tab: (cm) => {
        if (cm.getMode().name === 'null') {
          cm.execCommand('insertTab');
        } else {
          if (cm.somethingSelected()) {
            cm.execCommand('indentMore');
          } else {
            cm.execCommand('insertSoftTab');
          }
        }
      },
      Backspace: (cm) => {
        if (!cm.somethingSelected()) {
          let cursorsPos = cm.listSelections().map((selection) => selection.anchor);
          let indentUnit = cm.options.indentUnit;
          let shouldDelChar = false;

          for (let cursorPos of cursorsPos) {
            const { start, end, string, type } = cm.getTokenAt(cursorPos)
            if (type !== 'indent' && type !== null
              || start !== 0 || end !== cursorPos.ch || end === 0
              || string.trimStart() !== ''
              || end % indentUnit > 0) {
              shouldDelChar = true
              break
            }
          }

          if (!shouldDelChar) {
            cm.execCommand('indentLess');
          } else {
            cm.execCommand('delCharBefore');
          }
        } else {
          cm.execCommand('delCharBefore');
        }
      },
      'Shift-Tab': (cm) => cm.execCommand('indentLess')
    }
  }
})

保存アクションで dirty flag を消す

CodeMirror には、 CMD-S (CTRL-S) で保存するキーバインドがデフォルトで定義されており、その際呼び出されるアクションとして save という (カラの) アクションが定義されている。

また、 CodeMirror コンポーネント自体にも save という (カラの) メソッドが定義されている (上記のアクションとは別物)。

これらをくみあわせて、保存アクションをしたときに dirty flag をリセットするようにしたい。

ちなみに CodeMirror には開発者が活用できる以下のような内部状況フラグがある。

まずは save action の上書きから。

// default save action の上書き
VueCodemirror.CodeMirror.commands.save = (cm) => {
  // 保存されるときに "clean" 扱いにする
  cm.markClean()

  // 現状ではなにも内容の定義されていない、コンポーネントの save メソッドを呼び出しておく
  cm.save(cm)
}

つぎはコンポーネント側。

    codemirror#editor(ref="cmComponent" v-model="code" :options="cmOptions" @input="cmChanged")
export default {
  name: 'App',

  data() {
    return {
      code: '',
      isClean: true,

      cmOptions: {
        // ...
      }
    }
  },

  computed: {
    cmComponent() {
      return this.$refs.cmComponent
    }
  },

  mounted() {
    this.cmComponent.codemirror.save = (cm) => {
      this.isClean = cm.isClean()
    }
  },

  methods: {
    cmChanged() {
      this.isClean = this.cmComponent.codemirror.isClean()
    }
  }
}

コンポーネントの save メソッド側で、 手元のコンポーネントの clean フラグに CodeMirror の isClean フラグの状況を反映させる。 また、 CodeMirror コンポーネントの change 時にもそのフラグの状況を反映させる。

そもそも CodeMirror の clean フラグを使わなくっても、 CMD-S を自前でハンドリングしたり、 change のときだけ not clean にしたりすればいいんじゃ?って思うかもだけど、実は CodeMirror 側では、 Undo 等して、編集差分が発生しなくなったときに clean になったりする。なので、 clean フラグ自体は CodeMirror 側に負わせるのがよい。

(とはいえ、 save action とコンポーネントの save method をどちらもオーバライドしているこのやりかたはすこしくどすぎるかも。 save action の上書きだけでよさそう。あと、 change イベントと save action をハンドリングして内部のフラグをもってきているから、いまいちリアクティブみがない。もっとよい方法ないのかな。)

集大成として Markdown editor もどき

以上のサンプルコードとして、 Markdown editor もどきをつくった。

再掲になるけど、

github.com

https://user-images.githubusercontent.com/42583/77849162-8715a600-7204-11ea-8501-943fa39cdea4.png

べつだんリアルタイムプレビューを出すのはそこまで難しくないんだけど、 clean フラグのとりまわしをやりたかったので、 あえて CMD-S を押さないとプレビューに反映されないようになっている。

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 使ってる人はどうせニヤニヤしながら眺めているだけだろうから,ほんとにやりたい人は Getting started with XULRunner - Archive of obsolete content | MDN を参考に自分で適当にやっちゃってください!

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/((これは chrome/ フォルダからの相対パス指定だよ。)) って設定したので,chrome://myapp/content/main.xul という表記は,(chrome/)content/main.xul というファイルを指定していることになるんだ。ここはまあわかんなくてもok!

下の行は,のちほど dump() という関数でデバッグ用出力を可能にするための設定!そのた JavaScript コンソールにデバッグ情報を出力する方法もあるけど,その場合の設定は [https://developer.mozilla.org/ja/Debugging_a_XULRunner_Application: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

できた!やった!なんかうごいた!
ひみつボタンを押したら,はてなブックマークのソースコードが表示されたよー。

改造しよう!

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

てことは,普段ホームページ作りの時に「あぁこれ IE だと使えないからなぁ」
なんて諦めていたやつが色々つかえるかもしれないですね!
XPath とか Canvas とか,なんかそういうの!

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

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

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

謎なファイルって書いたけど,みんなも HTML を勉強するとき色々覚えたよね?
<input type="checkbox"> って書くとチェックボックスをだせるとか。
XUL の場合,その代わりにたとえば <checkbox> と書けばいいんだ。
つまり,新しいタグが増えたと思えばいいんだよ!
それに <colorpicker> とか素敵なウィジェットも揃っているよ!

くわしくは XUL controls - Mozilla | MDN をみてね!

  • アプリケーションの配布がたいへん

今回の Ubuntu Linux の場合,たいていは xulrunner はもともとインストールされている。
けど,Windows とかの場合は,ユーザに XULRunner をダウンロード*4させなきゃいけない。
(まぁ 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 の場合,アプリケーションの標準的な配布方法が定まっているのが大きいね!

参考になるページ

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

みんな作って、どんどこ公開しちゃえばいいんじゃないかな!
(できれば、ぼくが見て勉強できるようにソースコード付きで…!)

おわりに

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

あとは私感だよ!

  • main.xul ファイルに JavaScript とかボタンのラベルとかゴリゴリ書いたけど,これは行儀がよくないよ!
    • スクリプトは分離しよう!
    • ウィジェットのラベルなどは実体参照形式((HTML でいうところの &amp; みたいな書き方のことだよ!XUL だとこの DTD を自分で用意することで,自分オリジナルな実体宣言ができるんだ。あとは,その参照先は英語の場合「foobar」だよ,みたいに DTD に書けば,世界中のみんなから使ってもらえるアプリケーションになるよ!))で書くと国際化もできるようになるよ!
  • Firefox 拡張機能の書き方を勉強するより,XULRunner から書きはじめるほうが手軽な気がしたよ!
    • (拡張機能書いたことないけどね)
  • 「Firefox が動くところでは XUL アプリケーションが動く」,というのはなかなか Run Anywhere でいい感じだよ!
    • AIR の場合,たとえ Flash が動く環境でも Adobe が AIR ランタイムを移植してくれないと動かないよね。
  • はまちちゃんの書き方は真似しようと思ってもなかなか難しいよ!

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

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

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

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

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 UI の reset.cssfonts.css も使っています。

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

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

  • 配列に変換せずに一つの巨大な文字列から検索 → 切り出し。
  • クロージャで次の検索結果を取得する関数を保持しておいて、描画が必要になった時点で検索を実行。
    • その代わりにトータルのヒット件数がわかりません。
http://la.ma.la/blog/diary_200604021538.htm

な,なるほどー。

わたしのコードでは

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

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

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


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

よくある質問と答え

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

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

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

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

light じゃない版あったのカー→http://userscripts.org/scripts/show/37784。じゃあこれいらないや。

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

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

ほんとは http://white.s151.xrea.com/wiki/index.php?script/SBMCommentsViewer とか使えばいいんでしょうけど,結構ブクマの 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 使ってみたけどうまくいかなかったです

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() で落ちています。


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

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 の 汎用アニメーション関数の [http://docs.jquery.com/Effects/animate:title=animate()] って,アニメーションのターゲットとして指定できるのがスタイルだけなんです。

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

ちなみに DOM インタフェース経由でスタイルを直接いじっても反映されるというわけではないです。あくまで jQuery[http://docs.jquery.com/CSS/css#namevalue:title=css()][http://docs.jquery.com/Effects/animate:title=animate()] などの関数経由で指定できるようになったということです。また直接値を取得・設定する [http://docs.jquery.com/CSS/scrollTop:title=scrollTop()][http://docs.jquery.com/CSS/scrollTop:title=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

ようするに,Firefox と IE では,後方互換性モードの場合は <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 ではうまくアニメーションしないそうなので,ブラウザ避けをはさんであります。でもよくよく考えたらアニメーションしないだけでズバっと移動はしてくれるらしいのでブラウザ避けするまでもなかったかも。

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

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.

33 - API method to enumerate properties - v8 - Monorail

ええ,まったくもって 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


ともかく,ない袖は振れないので,http://d.hatena.ne.jp/tokuhirom/20080907/1220799397 で紹介されている 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 の仕様に合わせてあります。